延迟MessageBox(带自动关闭选项)






4.97/5 (25投票s)
此消息框通过禁用“确定”按钮并在设定的延迟时间内阻止其关闭。它还具有可选的自动关闭功能。有两个版本,一个版本使用 WH_CBT 挂钩和一些基本的窗口技巧(如子类化)来实现目标。另一个版本则更加 MFC 化。
概述
延迟消息框不会允许用户在延迟时间结束前关闭它。它通过在指定时间间隔到期之前禁用消息框上的“确定”按钮来实现。例如,这种情况可能适用于已过试用期的共享软件。假设您想向用户显示一个消息框,并希望确保它至少显示 10 秒钟。无论如何,Nish 最初编写此类时是出于一个完全不同的想法:他想将消息框居中在其父窗口上。那时他才发现,消息框默认会这样做。在他的情况下,之所以没有做到,是因为他将它们的所有者设置为桌面。无论如何,Nish 最终编写了一个具有自动关闭选项的延迟消息框。如果将自动关闭选项设置为 true
,则消息框将在延迟期结束后自行关闭。这就是 CDelayMessageBox
类的诞生。
原本只是一个尝试居中消息框的简单举动,最终却演变成了一些相当复杂的代码,包括 WH_CBT
挂钩、隐藏窗口、一个 CWnd*
到 HWND
的映射,以及一个重写了 DefWindowProc
的子类化消息框窗口。对于如此简单的任务来说,这似乎付出了很多努力。但这是一款“一次编写,多次使用”的类,因此 Nish 希望他的方法是合理的。
这时 Shog 对这个类产生了兴趣。Shog 是那种讨厌任何代码混淆的人,并且总是试图找出更简单的方法来做事。无论如何,他修改了 Nish 的类,使其更加 MFC 化。我们决定将其命名为 CDelayMessageBox2
,因为虽然它没有以任何方式扩展该类,但其实现却得到了彻底的重构。本文档中将同时介绍这两个类,以及演示项目和类源代码。您会发现以下文件对您来说很有价值。
- DelayMessageBox.cpp - 这是原始实现文件,如果您想查看使用
WH_CBT
挂钩和窗口子类化的基础示例,可以看看这个文件。 - DelayMessageBox.h - 原始类的头文件
- DelayMessageBox2.cpp - 这是新的实现文件,您可以在这里看到高质量的 MFC 类型子类化。
- DelayMessageBox2.h - 修改后的类的头文件。
用法
除了构造函数之外,CDelayMessageBox
和 CDelayMessageBox2
类只有一个公共方法。没有无参数构造函数。顺便说一句,在本文的其余部分,当您看到 CDelayMessageBox
时,它代表这两个类,除非另有明确说明。
构造函数
构造一个 CDelayMessageBox
对象。
CDelayMessageBox(CWnd* pParent);
pParent
- 这是将要显示的消息框的父窗口。您不应该将其设置为 NULL
。父窗口必须是一个有效的 CWnd
,它拥有一个有效的 HWND
。
注意 - 在 CDelayMessageBox2
中,您可以将 pParent
设置为 NULL
。
MessageBox 方法
显示消息框。
int MessageBox(
LPCTSTR lpszText,
int count,
bool bclose = false,
MBIcon icon = MBICONNONE );
lpszText
- 指向一个以 null 结尾的字符串,该字符串包含要显示的消息。您可以使用 CString
。
count
- 这是以秒为单位的延迟。您可以使用从 0 到 int
最大值的任何延迟,但建议出于实际目的将其保持在 60 以下。
bclose
- 如果设置为 true
,消息框将在延迟期后自行关闭;否则,“确定”按钮将被启用,以便用户可以手动关闭消息框。
icon
- 这是一个 MBIcon
枚举,可以接受以下值之一。
CDelayMessageBox::MBIcon::MBICONNONE
CDelayMessageBox::MBIcon::MBICONSTOP
CDelayMessageBox::MBIcon::MBICONQUESTION
CDelayMessageBox::MBIcon::MBICONEXCLAMATION
CDelayMessageBox::MBIcon::MBICONINFORMATION
示例代码
/* You may use either of the classes. In behaviour they are identical. It's in the implementation that they differ. */ //CDelayMessageBox mbox(this); CDelayMessageBox2 mbox(this); mbox.MessageBox(m_text, m_delay, m_close,(CDelayMessageBox2::MBIcon)mbicon);
技术细节
CDelayMessageBox
CDelayMessageBox
派生自 CWnd
,并在其构造函数中创建一个 CWnd
对象。创建的窗口具有唯一的标题文本并且是隐藏的。该唯一文本是一个 GUID。该类有一个静态 CMapPtrToPtr
成员,用于维护一个 HWND
到 CWnd*
的映射。这样,任何数量的线程都可以同时使用 CDelayMessageBox
类。换句话说,它是线程安全的。
当调用 MessageBox
方法时,我们使用 SetWindowsHookEx
来设置一个 WH_CBT
挂钩。然后我们以 1 秒的间隔启动一个计时器,并使用 CWnd::MessageBox
来显示我们的消息框。在挂钩过程中,我们使用 EnumChildWindows
枚举消息框窗口的所有子窗口。在 EnumChildWindows
的回调中,我们禁用“确定”按钮。我们还将消息框窗口子类化到一个自定义的 CWnd
派生类。我们还卸载 WH_CBT
挂钩。在计时器过程中,我们不断减少计数,并不断更改消息框的标题文本以反映剩余的秒数。当计数达到零时,我们启用“确定”按钮,或者如果自动关闭选项为 true
,我们通过发送 WM_CLOSE
消息来关闭消息框。
我们将其子类化的自定义 CWnd
派生类是为了修复 Andreas Saurwein 报告的一个 bug,他发现可以使用空格键关闭消息框。这是因为当按下空格键时,会向消息框窗口发送一个带有 BN_CLICKED
通知消息的 WM_COMMAND
消息。通过重写 DefWindowProc
并过滤掉这条消息来处理这个问题。
CDelayMessageBox2
现在我们没有隐藏的 CWnd
父窗口了。当调用 MessageBox(...)
时,我们使用 AfxHookWindowCreate
来创建 CWnd
对象。MFC 会为您调用 SetWindowsHookEx
。现在消息框已经被我们的 CWnd
派生类子类化了。我们重写 OnSubclassedInit
,禁用“确定”按钮并启动我们的计时器。我们还重写 OnCreate
,在其中调用 AfxUnhookWindowCreate
,因为我们不再需要该挂钩了。
计时器过程与原始类中的计时器过程类似,我们不断更改标题文本以反映剩余的秒数。延迟间隔结束后,我们向消息框发送一个 WM_CLOSE
消息。同时启用“确定”按钮。
类源代码列表
这里列出了新旧两个类。它们都使用截然不同的技术来解决延迟消息框问题。我们认为您可能想比较它们,这也可能有助于更好地理解其内部工作原理。现在这个类已经超出了其最初的价值,它现在是一个具有更多学术价值而非实用价值的类。这两种实现都揭示了 Windows 内部工作原理的很多内容。
头文件
旧版本
#pragma once // COkayWnd 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); };
新版本
#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++ 实现文件
旧版本
#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; } // COkayWnd IMPLEMENT_DYNAMIC(COkayWnd, CWnd) COkayWnd::COkayWnd() { } COkayWnd::~COkayWnd() { } BEGIN_MESSAGE_MAP(COkayWnd, CWnd) END_MESSAGE_MAP() // COkayWnd message handlers 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); }
新版本
#include "stdafx.h" #include "DelayMessageBox2.h" #include <afxpriv.h> IMPLEMENT_DYNAMIC(CDelayMessageBox2, CWnd) CDelayMessageBox2::CDelayMessageBox2(CWnd* pParent) { m_hWndParent = pParent->GetSafeHwnd(); // can be NULL 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() // Purpose: Unhook window creation int CDelayMessageBox2::OnCreate( LPCREATESTRUCT lpCreateStruct) { AfxUnhookWindowCreate(); return CWnd::OnCreate(lpCreateStruct); } // Purpose: Disable OK button, start timer 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; } // Purpose: display running countdown, close when finished. 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); } } } // Purpose: Display a message box, hooking it to do stuff 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); } // Purpose: compose a title for the dialog based // on the # of seconds left to disable it CString CDelayMessageBox2::FormTitle(int num) { CString s; s.Format("%d seconds remaining",num); return s; } // Purpose: prevent dialog from closing before // it has timed out 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); }
结论
这个类最初有一个想法,最后变成了另一个。这也是 Nish 首次尝试使用挂钩。所以他可能犯了一些错误的假设。但他寄希望于 CodeProject 数千名访问者和常客提供的精彩反馈。Shog 也希望看到是否有进一步简化此类的方法。谢谢。
更新和修复
- 2002 年 8 月 14 日 - Shog 已加入 Nish 作为联合作者,现在有一个更加 MFC 化的类版本可用。保留了这两个类,因为它们都展示了各种有趣的 Win32 技术。
- 2002 年 8 月 13 日 - Andreas Saurwein 报告了一个 bug,他发现可以使用空格键关闭延迟消息框。通过子类化消息框窗口并处理导致此行为的消息来修复此问题。