65.9K
CodeProject 正在变化。 阅读更多。
Home

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

2002年8月12日

Ms-PL

7分钟阅读

viewsIcon

309762

downloadIcon

5451

此消息框通过禁用“确定”按钮并在设定的延迟时间内阻止其关闭。它还具有可选的自动关闭功能。有两个版本,一个版本使用 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 - 修改后的类的头文件。

用法

除了构造函数之外,CDelayMessageBoxCDelayMessageBox2 类只有一个公共方法。没有无参数构造函数。顺便说一句,在本文的其余部分,当您看到 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 成员,用于维护一个 HWNDCWnd* 的映射。这样,任何数量的线程都可以同时使用 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,他发现可以使用空格键关闭延迟消息框。通过子类化消息框窗口并处理导致此行为的消息来修复此问题。
© . All rights reserved.