#include "shellprv.h" #define BALLOON_SHOW_TIME 15000 // 15 sec #define BALLOON_REPEAT_DELAY 10000 // 10 sec #define WM_NOTIFY_MESSAGE (WM_USER + 100) #define IDT_REMINDER 1 #define IDT_DESTROY 2 #define IDT_QUERYCANCEL 3 #define IDT_NOBALLOON 4 class CUserNotification : public IUserNotification { public: CUserNotification(); // IUnknown STDMETHODIMP QueryInterface(REFIID riid, void **ppv); STDMETHODIMP_(ULONG) AddRef(); STDMETHODIMP_(ULONG) Release(); // IUserNotification STDMETHODIMP SetBalloonInfo(LPCWSTR pszTitle, LPCWSTR pszText, DWORD dwInfoFlags); STDMETHODIMP SetBalloonRetry(DWORD dwShowTime, DWORD dwInterval, UINT cRetryCount); STDMETHODIMP SetIconInfo(HICON hIcon, LPCWSTR pszToolTip); STDMETHODIMP Show(IQueryContinue *pqc, DWORD dwContinuePollInterval); STDMETHODIMP PlaySound(LPCWSTR pszSoundName); private: ~CUserNotification(); static LRESULT CALLBACK s_WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); LRESULT CALLBACK _WndProc(UINT uMsg, WPARAM wParam, LPARAM lParam); HRESULT _GetWindow(); BOOL _SyncInfo(); BOOL _SyncIcon(); void _DelayDestroy(HRESULT hrDone); void _Timeout(); void _RemoveNotifyIcon(); LONG _cRef; HWND _hwnd; HICON _hicon; DWORD _dwShowTime; DWORD _dwInterval; UINT _cRetryCount; HRESULT _hrDone; DWORD _dwContinuePollInterval; IQueryContinue *_pqc; DWORD _dwInfoFlags; WCHAR *_pszTitle; WCHAR *_pszText; WCHAR *_pszToolTip; }; CUserNotification::CUserNotification() : _cRef(1), _cRetryCount(-1), _dwShowTime(BALLOON_SHOW_TIME), _dwInterval(BALLOON_REPEAT_DELAY), _dwInfoFlags(NIIF_NONE) { } CUserNotification::~CUserNotification() { Str_SetPtrW(&_pszToolTip, NULL); Str_SetPtrW(&_pszTitle, NULL); Str_SetPtrW(&_pszText, NULL); if (_hwnd) { _RemoveNotifyIcon(); DestroyWindow(_hwnd); } if (_hicon) DestroyIcon(_hicon); } STDMETHODIMP_(ULONG) CUserNotification::AddRef() { return InterlockedIncrement(&_cRef); } STDMETHODIMP_(ULONG) CUserNotification::Release() { ASSERT( 0 != _cRef ); ULONG cRef = InterlockedDecrement(&_cRef); if ( 0 == cRef ) { delete this; } return cRef; } HRESULT CUserNotification::QueryInterface(REFIID riid, void **ppv) { static const QITAB qit[] = { QITABENT(CUserNotification, IUserNotification), { 0 }, }; return QISearch(this, qit, riid, ppv); } HRESULT CUserNotification::SetBalloonInfo(LPCWSTR pszTitle, LPCWSTR pszText, DWORD dwInfoFlags) { Str_SetPtrW(&_pszTitle, pszTitle); Str_SetPtrW(&_pszText, pszText); _dwInfoFlags = dwInfoFlags; _SyncInfo(); // may fail if balloon _hwnd has not been created yet return S_OK; } HRESULT CUserNotification::SetBalloonRetry(DWORD dwShowTime, DWORD dwInterval, UINT cRetryCount) { if (-1 != dwShowTime) _dwShowTime = dwShowTime; if (-1 != dwInterval) _dwInterval = dwInterval; _cRetryCount = cRetryCount; return S_OK; } HRESULT CUserNotification::SetIconInfo(HICON hIcon, LPCWSTR pszToolTip) { if (_hicon) DestroyIcon(_hicon); if (hIcon == NULL) { _hicon = NULL; switch(_dwInfoFlags & NIIF_ICON_MASK) { case NIIF_INFO: _hicon = LoadIcon(NULL, IDI_INFORMATION); break; case NIIF_WARNING: _hicon = LoadIcon(NULL, IDI_WARNING); break; case NIIF_ERROR: _hicon = LoadIcon(NULL, IDI_ERROR); break; } } else { _hicon = CopyIcon(hIcon); } Str_SetPtrW(&_pszToolTip, pszToolTip); _SyncIcon(); return S_OK; } // returns: // S_OK // user clicked on the balloon or icon // S_FALSE // query continue callback (pcq) cancelled the notification UI // HRESULT_FROM_WIN32(ERROR_CANCELLED) // timeouts expired (user ignored the UI) HRESULT CUserNotification::Show(IQueryContinue *pqc, DWORD dwContinuePollInterval) { HRESULT hr = _GetWindow(); if (SUCCEEDED(hr)) { if (pqc) { _pqc = pqc; // don't need a ref since we don't return from here _dwContinuePollInterval = dwContinuePollInterval > 100 ? dwContinuePollInterval : 500; SetTimer(_hwnd, IDT_QUERYCANCEL, _dwContinuePollInterval, NULL); } // if there is no balloon info specified then there won't be a "balloon timeout" event // thus we need to do this ourselves. this lets people use this object for non balloon // notification icons if ((NULL == _pszTitle) && (NULL == _pszText)) { SetTimer(_hwnd, IDT_NOBALLOON, _dwShowTime, NULL); } MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } hr = _hrDone; if (pqc) { KillTimer(_hwnd, IDT_QUERYCANCEL); // in case any are in the queue _pqc = NULL; // to avoid possible problems } } return hr; } HRESULT CUserNotification::PlaySound(LPCWSTR pszSoundName) { SHPlaySound(pszSoundName); return S_OK; } // take down our notification icon void CUserNotification::_RemoveNotifyIcon() { NOTIFYICONDATA nid = { sizeof(nid), _hwnd, 0 }; Shell_NotifyIcon(NIM_DELETE, &nid); } // the balloon related data (title, body test, dwInfoFlags, timeout BOOL CUserNotification::_SyncInfo() { BOOL bRet = FALSE; if (_hwnd) { NOTIFYICONDATA nid = { sizeof(nid), _hwnd, 0, NIF_INFO }; if (_pszTitle) { StringCchCopy(nid.szInfoTitle, ARRAYSIZE(nid.szInfoTitle), _pszTitle); // ok to truncate } if (_pszText) { StringCchCopy(nid.szInfo, ARRAYSIZE(nid.szInfo), _pszText); // ok to truncate } nid.dwInfoFlags = _dwInfoFlags; nid.uTimeout = _dwShowTime; bRet = Shell_NotifyIcon(NIM_MODIFY, &nid); } return bRet; } BOOL CUserNotification::_SyncIcon() { BOOL bRet = FALSE; if (_hwnd) { NOTIFYICONDATA nid = { sizeof(nid), _hwnd, 0, NIF_ICON | NIF_TIP}; nid.hIcon = _hicon ? _hicon : LoadIcon(NULL, IDI_WINLOGO); if (_pszToolTip) { StringCchCopy(nid.szTip, ARRAYSIZE(nid.szTip), _pszToolTip); // ok to truncate } bRet = Shell_NotifyIcon(NIM_MODIFY, &nid); } return bRet; } HRESULT CUserNotification::_GetWindow() { HRESULT hr = S_OK; if (NULL == _hwnd) { _hwnd = SHCreateWorkerWindow(s_WndProc, NULL, 0, 0, NULL, this); if (_hwnd) { NOTIFYICONDATA nid = { sizeof(nid), _hwnd, 0, NIF_MESSAGE, WM_NOTIFY_MESSAGE }; if (Shell_NotifyIcon(NIM_ADD, &nid)) { _SyncIcon(); _SyncInfo(); } else { DestroyWindow(_hwnd); _hwnd = NULL; hr = E_FAIL; } } } return hr; } void CUserNotification::_DelayDestroy(HRESULT hrDone) { _hrDone = hrDone; SetTimer(_hwnd, IDT_DESTROY, 250, NULL); } void CUserNotification::_Timeout() { if (_cRetryCount) { _cRetryCount--; SetTimer(_hwnd, IDT_REMINDER, _dwInterval, NULL); } else { // timeout, same HRESULT as user cancel _DelayDestroy(HRESULT_FROM_WIN32(ERROR_CANCELLED)); } } LRESULT CALLBACK CUserNotification::_WndProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { LRESULT lres = 0; switch (uMsg) { case WM_NCDESTROY: SetWindowLongPtr(_hwnd, 0, NULL); _hwnd = NULL; break; case WM_TIMER: KillTimer(_hwnd, wParam); // make all timers single shot switch (wParam) { case IDT_REMINDER: _SyncInfo(); break; case IDT_DESTROY: _RemoveNotifyIcon(); PostQuitMessage(0); // exit our msg loop break; case IDT_QUERYCANCEL: if (_pqc && (S_OK == _pqc->QueryContinue())) SetTimer(_hwnd, IDT_QUERYCANCEL, _dwContinuePollInterval, NULL); else _DelayDestroy(S_FALSE); // callback cancelled break; case IDT_NOBALLOON: _Timeout(); break; } break; case WM_NOTIFY_MESSAGE: switch (lParam) { case NIN_BALLOONSHOW: case NIN_BALLOONHIDE: break; case NIN_BALLOONTIMEOUT: _Timeout(); break; case NIN_BALLOONUSERCLICK: case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: _DelayDestroy(S_OK); // user click break; default: break; } break; default: lres = DefWindowProc(_hwnd, uMsg, wParam, lParam); break; } return lres; } LRESULT CALLBACK CUserNotification::s_WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CUserNotification *pun = (CUserNotification *)GetWindowLongPtr(hwnd, 0); if (WM_CREATE == uMsg) { CREATESTRUCT *pcs = (CREATESTRUCT *)lParam; pun = (CUserNotification *)pcs->lpCreateParams; pun->_hwnd = hwnd; SetWindowLongPtr(hwnd, 0, (LONG_PTR)pun); } return pun ? pun->_WndProc(uMsg, wParam, lParam) : DefWindowProc(hwnd, uMsg, wParam, lParam); } STDAPI CUserNotification_CreateInstance(IUnknown* punkOuter, REFIID riid, void **ppv) { HRESULT hr; CUserNotification* p = new CUserNotification(); if (p) { hr = p->QueryInterface(riid, ppv); p->Release(); } else { hr = E_OUTOFMEMORY; *ppv = NULL; } return hr; }