Overview
A delay message box will not allow the user to dismiss it until the delay
interval has run out. It does this by disabling the OK button on the message box
till the specified time interval expires. One example of a situation where this
might be useful is a shareware program that has expired its trial period. Say you
want to show a message box to the user and you want to make sure it stays there
for at least 10 seconds. Anyway Nish started writing this class with a totally
different idea. He wanted to center his message box on it's parent window. That's
when he found out that, message boxes do this by default. In his case they were
not doing so because he had made them owned by the desktop. Anyway Nish ended up
writing a delay message box that also has an auto-close option. If the
auto-close option is set to true
, then the message box will close on it's own
once the delay period has terminated. This was how the CDelayMessageBox
class was born.
What turned out as a simple attempt to center a message box has ended up in
some rather complicated code with WH_CBT
hooks, invisible windows,
a CWnd*
to HWND
map and a sub-classed message box
window that overrides DefWindowProc
of all things to override. It
sure seems like a lot of work for such a simple sounding task. But it is the
write-once-use-multiple-times kind of class and thus Nish hopes his methods are
justified.
That's when Shog got interested in the class. Shog is the type of guy who
hates any kind of code obfuscation and he's always trying to figure out easier
ways of doing things. Anyhow he modified Nish's class so that it was more
MFC-ied. We decided to call it CDelayMessageBox2
because while it
didn't extend the class in anyway, the implementation was thoroughly revamped.
Both the classes are presented in this article as well as in the demo project
and the class source. You'll find the following files to be of interest to you.
- DelayMessageBox.cpp - This was the original implementation file and
you can take a look at this one if you are interested in seeing elementary
examples of the use of
WH_CBT
hooks and window sub-classing.
- DelayMessageBox.h - The header file for the original class
- DelayMessageBox2.cpp - This is the new implementation file
and you can see some high quality MFC type sub classing here.
- DelayMessageBox2.h - The header file for the revised class.
Usage
The CDelayMessageBox
and CDelayMessageBox2
classes have only one public method in addition to the
constructor. There is no parameter-less constructor. By the way, in the rest of
the article when you see CDelayMessageBox
, it represents both the
classes unless specifically mentioned otherwise.
Constructor
Constructs a CDelayMessageBox
object.
CDelayMessageBox(CWnd* pParent);
pParent
- This will be the parent window of the eventual message box that
will be displayed. You should not set this to NULL
. The parent window must be a
valid CWnd
that holds a valid HWND
.
Note - In CDelayMessageBox2
you can set pParent
to
NULL
.
MessageBox method
Displays the message box.
int MessageBox(
LPCTSTR lpszText,
int count,
bool bclose = false,
MBIcon icon = MBICONNONE );
lpszText
- Points to a null-terminated string containing the message to be
displayed. You may use a CString
here.
count
- This is the delay in seconds. You can use any delay from 0 - the
maximum size of an int
, but you are advised to keep it under 60 for all
practical purposes.
bclose
- If this is set to true
, the message box will close on its own after
the delay period, otherwise the OK button is enabled so that the user can
dismiss the message box manually.
icon
- This is an MBIcon
enumeration which can take one of the following
values.
CDelayMessageBox::MBIcon::MBICONNONE
CDelayMessageBox::MBIcon::MBICONSTOP
CDelayMessageBox::MBIcon::MBICONQUESTION
CDelayMessageBox::MBIcon::MBICONEXCLAMATION
CDelayMessageBox::MBIcon::MBICONINFORMATION
Sample Code
CDelayMessageBox2 mbox(this);
mbox.MessageBox(m_text,
m_delay,
m_close,(CDelayMessageBox2::MBIcon)mbicon);
Technical details
CDelayMessageBox
CDelayMessageBox
is derived from CWnd
and it creates a
CWnd
object in it's
constructor. The window that is created has a unique title text and is hidden.
The unique text is a GUID. The class has a static CMapPtrToPtr
member using
which we maintain an HWND
to CWnd*
map. This is so that any number of threads
may simultaneously use the CDelayMessageBox
class. In other words it's
thread-safe.
When the MessageBox
method is called we use SetWindowsHookEx
to set a
WH_CBT
hook. We then start a timer at a 1-second interval and use CWnd::MessageBox
to
show our message box. In the hook proc we enumerate all the child windows of the
message box window using EnumChildWindows
. In the callback for
EnumChildWindows
,
we disable the OK button. We also subclass the message box window to a custom
CWnd
derived class. And we also unhook the WH_CBT
hook. In the timer proc,
we keep decreasing the count and also keep changing the title text of the
message box to reflect the remaining time in seconds. When the count reaches
zero we enable the OK button or if the auto-close option is true
we dismiss the
message box using a WM_CLOSE
message.
The custom CWnd
derived class into which we subclass the message
box was added as a bug fix to a problem reported by Andreas Saurwein where he
found that the message box can be closed using the space bar. This is because a
WM_COMMAND
message is sent to the message box window with a
BN_CLICKED
notification when the space bar is pressed. This has been
handled by overriding DefWindowProc
and filtering out this message.
CDelayMessageBox2
Now we don't have a hidden CWnd
parent for the message box. We
create the CWnd
object using AfxHookWindowCreate
when
the call to MessageBox(...)
is made. MFC will call
SetWindowsHookEx
for you. Now the message box has been sub-classed by our
CWnd
derived class. We override OnSubclassedInit
and
we disable the OK button and start our timer. We also override OnCreate
where we call AfxUnhookWindowCreate
as we don't have any further
need for the hook.
The timer proc is similar to the timer proc in the original class and we keep
changing the title text to reflect the remaining time in seconds. Once the delay
interval has elapsed we post a WM_CLOSE
to the message box. And
also enable the OK button.
Class source listings
Both the old and new classes are listed here. They both use contrastingly
different techniques to solve the delay message box problem. We thought you
might want to compare them and that might also help to understand the inner
workings better. Now the class has out-valued itself in the sense, it is now a
class with a lot more academic value than utility. Both the implementations
reveal a lot about the inner workings of Windows.
Header files
Old one
#pragma once
class COkayWnd : public CWnd
{
DECLARE_DYNAMIC(COkayWnd)
public:
COkayWnd();
virtual ~COkayWnd();
protected:
DECLARE_MESSAGE_MAP()
public:
protected:
virtual LRESULT DefWindowProc(UINT message,
WPARAM wParam, LPARAM lParam);
};
class CDelayMessageBox : public CWnd
{
DECLARE_DYNAMIC(CDelayMessageBox)
public:
CDelayMessageBox(CWnd* pParent);
virtual ~CDelayMessageBox();
enum MBIcon
{
MBICONNONE = 0,
MBICONSTOP = MB_ICONSTOP,
MBICONQUESTION = MB_ICONQUESTION,
MBICONEXCLAMATION = MB_ICONEXCLAMATION,
MBICONINFORMATION = MB_ICONINFORMATION
};
int MessageBox(LPCTSTR lpszText, int count,
bool bclose = false, MBIcon icon = MBICONNONE );
protected:
HHOOK m_hHook;
HWND m_hMsgBoxWnd;
HWND m_hOK;
int m_count;
bool m_autoclose;
COkayWnd m_OkayWnd;
static LRESULT CALLBACK CBTProc(int nCode,
WPARAM wParam, LPARAM lParam);
static BOOL CALLBACK EnumChildProc( HWND hwnd,
LPARAM lParam );
static CMapPtrToPtr m_map;
CString FormTitle(int num);
protected:
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnTimer(UINT nIDEvent);
};
New one
#pragma once
class CDelayMessageBox2 : public CWnd
{
DECLARE_DYNAMIC(CDelayMessageBox2)
public:
CDelayMessageBox2(CWnd* pParent);
enum MBIcon
{
MBICONNONE = 0,
MBICONSTOP = MB_ICONSTOP,
MBICONQUESTION = MB_ICONQUESTION,
MBICONEXCLAMATION = MB_ICONEXCLAMATION,
MBICONINFORMATION = MB_ICONINFORMATION
};
int MessageBox(LPCTSTR lpszText, int count,
bool bclose = false, MBIcon icon = MBICONNONE );
protected:
int m_count;
bool m_autoclose;
HWND m_hWndParent;
CString FormTitle(int num);
virtual LRESULT DefWindowProc(UINT message,
WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OnSubclassedInit(WPARAM wParam,
LPARAM lParam);
afx_msg int OnCreate(
LPCREATESTRUCT lpCreateStruct);
afx_msg void OnTimer(UINT nIDEvent);
DECLARE_MESSAGE_MAP()
};
C++ implementation files
Old one
#include "stdafx.h"
#include "DelayMessageBox.h"
CMapPtrToPtr CDelayMessageBox::m_map;
IMPLEMENT_DYNAMIC(CDelayMessageBox, CWnd)
CDelayMessageBox::CDelayMessageBox(CWnd* pParent)
{
m_hHook = NULL;
m_hMsgBoxWnd = NULL;
m_hOK = NULL;
m_autoclose = NULL;
m_OkayWnd.m_hWnd = NULL;
Create(NULL,
"{8B32A21C-C853-4785-BE20-A4E575EE578A}",
WS_OVERLAPPED, CRect(0,0,0,0),
pParent,1000);
m_map[m_hWnd] = this;
}
CDelayMessageBox::~CDelayMessageBox()
{
m_map.RemoveKey(m_hWnd);
DestroyWindow();
}
BEGIN_MESSAGE_MAP(CDelayMessageBox, CWnd)
ON_WM_TIMER()
END_MESSAGE_MAP()
BOOL CALLBACK CDelayMessageBox::EnumChildProc(
HWND hwnd, LPARAM lParam )
{
CDelayMessageBox *pthis =
static_cast<CDelayMessageBox*>((LPVOID)lParam);
char str[256];
::GetWindowText(hwnd,str,255);
if(strcmp(str,"OK") == 0)
{
pthis->m_hOK = hwnd;
if(pthis->m_count>0)
{
::EnableWindow(pthis->m_hOK,FALSE);
}
return FALSE;
}
return TRUE;
}
LRESULT CALLBACK CDelayMessageBox::CBTProc(
int nCode,WPARAM wParam, LPARAM lParam)
{
if (nCode == HCBT_ACTIVATE )
{
void* p;
m_map.Lookup(::FindWindowEx(::GetParent(
(HWND)wParam),NULL,NULL,
"{8B32A21C-C853-4785-BE20-A4E575EE578A}"),p);
CDelayMessageBox* pthis = (CDelayMessageBox*)p;
pthis->m_hMsgBoxWnd = (HWND)wParam;
EnumChildWindows(pthis->m_hMsgBoxWnd,
EnumChildProc,(LPARAM)pthis);
UnhookWindowsHookEx(pthis->m_hHook);
if(pthis->m_count>0)
pthis->m_OkayWnd.SubclassWindow(
pthis->m_hMsgBoxWnd);
pthis->m_hHook = NULL;
}
return FALSE;
}
void CDelayMessageBox::OnTimer(UINT nIDEvent)
{
if(nIDEvent == 100 && m_hMsgBoxWnd )
{
if(m_count>0)
m_OkayWnd.SetWindowText(FormTitle(--m_count));
if(m_count == 0)
{
if(m_OkayWnd.m_hWnd)
{
m_OkayWnd.UnsubclassWindow();
m_OkayWnd.m_hWnd = NULL;
}
::EnableWindow(m_hOK,TRUE);
KillTimer(100);
m_hOK = NULL;
if(m_autoclose)
::PostMessage(m_hMsgBoxWnd,WM_CLOSE,0,0);
m_hMsgBoxWnd = NULL;
}
}
CWnd::OnTimer(nIDEvent);
}
int CDelayMessageBox::MessageBox(LPCTSTR lpszText,
int count, bool bclose,MBIcon icon)
{
m_autoclose = bclose;
m_hHook = SetWindowsHookEx(WH_CBT,CBTProc,
AfxGetApp()->m_hInstance,
AfxGetApp()->m_nThreadID);
m_count = count;
SetTimer(100,1000,NULL);
CWnd::MessageBox(lpszText,FormTitle(m_count),icon);
return IDOK;
}
CString CDelayMessageBox::FormTitle(int num)
{
CString s;
s.Format("%d seconds remaining",num);
return s;
}
IMPLEMENT_DYNAMIC(COkayWnd, CWnd)
COkayWnd::COkayWnd()
{
}
COkayWnd::~COkayWnd()
{
}
BEGIN_MESSAGE_MAP(COkayWnd, CWnd)
END_MESSAGE_MAP()
LRESULT COkayWnd::DefWindowProc(UINT message,
WPARAM wParam, LPARAM lParam)
{
if(message == WM_COMMAND)
{
if(HIWORD(wParam) == BN_CLICKED )
return 0;
}
return CWnd::DefWindowProc(message, wParam, lParam);
}
New one
#include "stdafx.h"
#include "DelayMessageBox2.h"
#include <afxpriv.h>
IMPLEMENT_DYNAMIC(CDelayMessageBox2, CWnd)
CDelayMessageBox2::CDelayMessageBox2(CWnd* pParent)
{
m_hWndParent = pParent->GetSafeHwnd();
m_autoclose = NULL;
m_count = 0;
}
BEGIN_MESSAGE_MAP(CDelayMessageBox2, CWnd)
ON_WM_TIMER()
ON_WM_CREATE()
ON_MESSAGE(WM_INITDIALOG, OnSubclassedInit)
END_MESSAGE_MAP()
int CDelayMessageBox2::OnCreate(
LPCREATESTRUCT lpCreateStruct)
{
AfxUnhookWindowCreate();
return CWnd::OnCreate(lpCreateStruct);
}
LRESULT CDelayMessageBox2::OnSubclassedInit(
WPARAM wParam, LPARAM lParam)
{
LRESULT lRet = Default();
CWnd* pOk = GetDlgItem(IDCANCEL);
if ( NULL != pOk )
pOk->EnableWindow(FALSE);
SetTimer(100,1000,NULL);
return lRet;
}
void CDelayMessageBox2::OnTimer(UINT nIDEvent)
{
if (nIDEvent == 100)
{
if (m_count>0)
SetWindowText(FormTitle(--m_count));
if (m_count == 0)
{
CWnd* pOk = GetDlgItem(IDCANCEL);
if ( NULL != pOk )
{
pOk->EnableWindow(TRUE);
pOk->SetFocus();
}
KillTimer(100);
if (m_autoclose)
PostMessage(WM_CLOSE,0,0);
}
}
}
int CDelayMessageBox2::MessageBox(LPCTSTR lpszText,
int count, bool bclose,MBIcon icon)
{
m_autoclose = bclose;
m_count = count;
AfxHookWindowCreate(this);
return ::MessageBox(m_hWndParent,
lpszText, FormTitle(m_count), icon);
}
CString CDelayMessageBox2::FormTitle(int num)
{
CString s;
s.Format("%d seconds remaining",num);
return s;
}
LRESULT CDelayMessageBox2::DefWindowProc(
UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == WM_COMMAND && m_count > 0)
{
if(HIWORD(wParam) == BN_CLICKED )
return 0;
}
return CWnd::DefWindowProc(message, wParam, lParam);
}
Conclusion
This class started off with one idea and ended up with another. This was also
one of Nish's first proper attempts with using hooks. So he might have made some
erroneous assumptions. But he is counting on the wonderful feedback that is
available through the thousands of CodeProject visitors and regulars.
Shog would also like to see whether there is any way to further simply the
class. Thank you.
Updates and fixes
- Aug 14 2002 - Shog has joined Nish as co-author and now there is a more
MFC-ied version of the class available. Both classes have been retained as
they both depict various interesting win32 techniques.
- Aug 13 2002 - A bug was reported by Andreas Saurwein where he discovered
that the space bar can close the delayed message box. This has been fixed by
sub-classing the message box window and handling the message that causes this
behaviour.