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

消息管理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (60投票s)

2000年5月17日

viewsIcon

439054

学习管理用户定义消息的有效方法。

引言

作为程序员,您可以定义两种消息。第一种是编译时常量类型。经过多年的经验,我发现这些并不是你的朋友。尽管如此,我还是会描述它们,因为许多人仍然喜欢它们,但就个人而言,我尽可能地避免它们。第二种是保证在系统内独一无二的消息。我更喜欢这些,并且只使用它们。


WM_USER:已废弃

较早的 Windows 编程书籍介绍了如何使用符号 WM_USER 定义用户定义消息。这种技术已经过时。基于 WM_USER 的符号与 Microsoft 正在使用的消息冲突问题太多。新的方法是使用 WM_APP 作为基数。如果您有使用 WM_USER 的内容,其用法与基于 WM_APP 的消息用法相同。因此,我不会在此处重复讨论。


WM_APP:常量消息

如果您对编译时常量消息的概念感到满意——在您阅读下一节之后,您可能就不满意了——那么您可以使用基于符号 WM_APP 的定义,Microsoft 现在将其指定为理想的符号。这种符号定义的正确形式是

#define UWM_MYMESSAGE (WM_APP + n)

其中 n 是某个整数,通常是像 1、2、3 等小整数。这定义了一个标识消息的值。虽然严格来说括号不是强制性的,但良好的编程习惯要求它们的存在。

我更喜欢一种不与 Microsoft 命名约定冲突的命名约定。一方面,它使您的代码难以被其他人阅读和理解;另一方面,它也使您自己难以阅读和理解。我的偏好是使用 UWM_ 前缀(用户窗口消息);其他人使用 WMU_(窗口消息,用户),您可以选择任何您想要的约定,但请不要使用与 Microsoft 已使用的前缀冲突的前缀。

请注意,绝对没有要求每个用户定义消息都是唯一的。消息始终在特定窗口的上下文中解释。因此,您可以有以下消息

#define UWM_PAINT_VIEW_PURPLE (WM_APP + 7)
#define UWM_RESET_VIEW        (WM_APP + 7)

这些是完全有效的,前提是接受紫色请求的视图永远不会收到重置请求,反之亦然。

要在表中创建条目以调度这些消息,您可以创建一个条目

ON_MESSAGE(UWM_RESET_VIEW, OnReset)

这要求您定义处理程序 OnReset。在 .h 文件的消息处理程序部分,您添加声明

afx_msg LRESULT OnReset(WPARAM, LPARAM);

当您的窗口类收到 UWM_RESET_VIEW 消息时,它将调用 OnReset 处理程序。


注册窗口消息:非常量消息

常量消息存在几个问题。

  • 您无法可靠地在进程之间发送它们。如果您不小心将消息发送到从未听说过您的消息的进程,它可能会崩溃。如果您收到一条您认为您理解的消息,您可能会崩溃。
  • 您无法创建通过消息通知其客户端的 DLL。这是因为您可能选择 (WM_APP+7) 作为您想要的消息,而您从未听说过的某个其他 DLL 编写者可能也选择了 (WM_APP+7) 作为他或她想要的消息。试图使用这两个 DLL 的可怜程序员会陷入困境,因为存在冲突。
  • 您甚至无法考虑使用 SendMessageToDescedants 将这些消息中的一个通过窗口层次结构发送下去,因为您从未听说过的某个窗口可能正在使用该消息。考虑前一节中的示例,其中将视图绘制成紫色和重置视图的消息是相同的代码。如果您将此消息发送给主框架的所有子孙,有些会重置,有些会变成紫色,这不是特别理想的结果。

解决此问题的方法是使用注册窗口消息。这是一个保证唯一的 संदेश。只有创建它的那些窗口、进程或 DLL,以及那些专门使用它的,才会实际拥有相同的消息编号。

这是如何做到的?

有一个消息范围,0xC000 到 0xEFFF,保留用于注册窗口消息。当您调用 API 函数 ::RegisterWindowMessage 时,您会传递一个字符串。它会在内部表中查找该字符串。如果找到该字符串,它将返回已分配的整数。如果未找到该字符串,它会在表中创建新条目,从 0xC000 到 0xEFFF 范围分配一个新的整数值,并返回该整数值。存储这些字符串的表对于机器上的所有进程都是全局的,因此如果两个完全不同的程序注册相同的字符串,它们都会获得相同的整数。它们现在可以通过这些消息相互通信。

不,您不能“取消注册”消息。您不需要这样做。

因此,用户定义消息的一种简单形式是声明一个变量,我通常在每个使用它的模块中将其声明为静态。

static const UINT UWM_RESET_VIEW = 
        ::RegisterWindowMessage(_T("UWM_RESET_VIEW"));

我稍后会告诉你为什么这仍然不够充分,但目前把它作为一个工作示例。

处理注册消息的方式与处理常量用户定义消息的方式完全相同。宏略有不同,但其余的处理方式相同。将该行添加到您的 MESSAGE_MAP

ON_REGISTERED_MESSAGE(UWM_RESET_VIEW, OnReset)

与常量消息一样,您需要在 .h 文件的类处理程序部分添加声明来定义 OnReset 处理程序。

afx_msg LRESULT OnReset(WPARAM, LPARAM);

注册窗口消息和常量用户定义消息的处理程序是完全相同的。实际上,在处理程序中,您无法真正判断程序员是使用一个还是另一个来重写代码的。


但它是唯一的吗??

您可能已经发现了一个对常量消息和注册消息都相同的问题。如果您选择了一个漂亮、明显的名称,例如“UWM_RESET_VIEW”作为消息的字符串名称,而其他程序员也为他或她的 DLL 选择了一个漂亮、明显、简单的名称,例如“UWM_RESET_VIEW”。我们真的取得了进展吗?

不支持。

但有一个解决方法。SDK 中有一个名为 GUIDGEN 的程序。这个程序的作用是创建一个唯一的 128 位二进制值。每次您创建一个新的唯一标识符(GUID)时,您都可以确信它是真正、真实的、唯一的。它不仅对您是唯一的,而且对所有地方、所有人、所有时间都是唯一的。它包含了日期和时间、您网卡上的 MAC 地址(如果您没有网卡,它会使用另一种方法,与其他 GUID 冲突的概率约为 263 分之一),以及一堆其他信息。因此,除了两个程序员之间明确串通之外,他们没有办法使用相同的 GUID。

我所有的注册窗口消息都使用 GUID 作为其名称的一部分。

当您运行 GUIDGEN 时,您会看到一个这样的屏幕

选择选项 4,“注册表格式”。单击“复制”按钮。结果框中显示的字符串副本将被放置在剪贴板中。然后转到您的编辑器并输入类似以下内容:

#define UWM_RESET_VIEW_MSG _T("UWM_RESET_VIEW-<paste>")

这将创建一个类似以下内容名称

_T("UWM_RESET_VIEW-{4E7F6EC0-6ADC-11d3-BC36-006067709674}")

我实际上有一个宏

#define DECLARE_USER_MESSAGE(name) \
     static const UINT name = ::RegisterWindowMessage(name##_MSG);

这为我解决了大部分麻烦。虽然严格来说,我的 GUID 足以用于所有消息,但我通常只为每条消息生成一个新的 GUID。

每当我想创建一个消息实例,用于消息表或发布时,我只需这样做

DECLARE_USER_MESSAGE(UWM_RESET_VIEW)

而且,由于我习惯于将字符串命名为相同,因此变量被初始化。通常,这些变量出现在使用符号的每个模块的外部块中。我只需要确保包含 DECLARE_USER_MESSAGE 的头文件和包含我想要的消息的头文件。

那么我为什么要费心在字符串中放入一个可读的名称呢?128 位的 GUID 应该足够了!嗯,事实是可读的名称是完全多余的,而且基本上不相关。除非您碰巧使用 Spy++ 来跟踪消息流量。在这种情况下,您真的,真的希望看到理解的东西,而 128 位的 GUID 并不是容易阅读的值列表中的首选。可读字符串存在的唯一原因是方便您,开发者。否则,它无关紧要。


定义消息

用户定义消息是一种接口。作为接口,它们需要被定义。我的文本编辑器中有一些宏可以简化此操作。它们为我生成一个标准头文件,我填充它。头文件看起来像下面的示例。


示例 1:一个具有简单参数,并且已发送但响应无关紧要的消息

/***************************************************************
*                           UWM_COLOR_IT
* Inputs:
*       WPARAM: ignored, 0
*       LPARAM: RGB value to use for coloring
* Result: LRESULT
*	Logically void, 0, always
* Effect:
*	Causes the view to repaint itself in the specified color
***************************************************************/
#define UWM_COLOR_IT_MSG _T("UWM_COLOR_IT-{4E7F6EC1-6ADC-11d3-BC36-006067709674}")

示例 2:一个没有参数,为了获取结果而发送的消息

/***************************************************************
*                           UWM_QUERY_CUT
* Inputs:
*       WPARAM: ignored, 0
*       LPARAM: ignored, 0
* Result: LRESULT
*	(LRESULT)(BOOL) TRUE if a cut operation is possible
*                       FALSE if there is no selection
***************************************************************/
#define UWM_QUERY_CUT_MSG _T("UWM_QUERY_CUT-{4E7F6EC3-6ADC-11d3-BC36-006067709674}")

示例 3:具有复杂参数并返回有趣值的消息

/***************************************************************
*                           UWM_SET_COORD
* Inputs:
*       WPARAM: (WPARAM)(BOOL) FALSE for absolute
*                              TRUE for relative
*       LPARAM: (LPARAM)MAKELONG(x, y)
* Result: LRESULT
*	(LRESULT)MAKELONG(x, y) previous coordinate value
* Effect:
*	Sets the coordinate in the view, and returns the previous
*       coordinate.
* Notes:
*	The x,y values are added to the current position if
*	WPARAM is TRUE, otherwise they replace the current
*	position.
***************************************************************/
#define UWM_SET_COORD_MSG _T("UWM_SET_COORD-{4E7F6EC2-6ADC-11d3-BC36-006067709674}")

请注意,我仔细记录了获取所需 WPARAMLPARAM 值所需的类型转换。然后我知道在编写方法时应该如何转换它。这是一个另一个消息处理程序的示例,它接受一个指向对象的指针。

/***************************************************************
*                       CMyView::OnAssignMyInfo
* Inputs:
*       WPARAM: ignored, 0
*       LPARAM: (LPARAM)(LPMYINFO)
* Result: LRESULT
*	Logically void, 0, always
* Effect:
*	Modifies the current view by the values in LPMYINFO
* Notes:
*	If LPMYINFO.IsValidCount is FALSE, the count field is
*	not modified
***************************************************************/
LRESULT CMyView::OnAssignMyInfo(WPARAM, LPARAM lParam)
{
    LPMYINFO info = (LPMYINFO)lParam;
    visible = info.visible;
    if(info.IsValidCount)
        count = info.count;
    return 0; // value is ignored
}

在消息中传递指针

当您将指针作为 WPARAMLPARAM 传递时,您需要小心传递什么以及如何传递。关键在于您是执行 SendMessage 还是 PostMessage。如果您执行 SendMessage,则可以使用堆栈上的对象引用、静态存储中的对象或堆上的对象。这是因为在处理程序完成其操作之前,执行 SendMessage 的线程中的控制不会恢复。特别是,这意味着在消息处理期间,任何引用堆栈的地址都保持有效。这不适用于跨进程消息!见下文!

但是,如果您计划使用 PostMessage 传递对象指针,那么您必须始终使用静态或基于堆的对象。在这种情况下,基于堆栈的对象的地址是无意义的。这是因为执行 PostMessage 的函数很可能在消息处理之前很久就返回了——事实上,如果它正在向同一线程发布消息,它必须在消息处理之前返回。这意味着对堆栈的对象引用指向有效空间,但该空间可能已被覆盖。如果堆栈上的对象是具有析构函数的 C++ 对象,则将调用析构函数,并且堆栈上对象内部的对象可能会被释放。例如,您不能在以下上下文中使用 PostMessage

{
    CString s;
    // ... assign a value to s
	PostMessage(UWM_LOG_MESSAGE, 0, (LPARAM)&s);
}

即使后续调用未覆盖堆栈上引用的地址,CString 析构函数也会释放字符串引用的数据。当调用处理程序并尝试引用字符串时,您将得到完全不正确的数据和访问故障之间的某种效果。任何按您预期工作的情况都非常渺茫。

然而,下面的代码是正确的。我们首先看定义

/***************************************************************
*                       UWM_LOG_MESSAGE
* Inputs:
*       WPARAM: ignored, 0
*       LPARAM: (LPARAM)(CString *): String to log
* Result: LRESULT
*	Logically void, 0, always
* Effect:
*	Logs the message in the debug output window
* Notes:
*	The CString object must explicitly deallocated by
*	the handler for this message.
*	This message is usually sent via PostMessage. If sent
*	via SendMessage, the sender must not delete the
*	CString, nor should it assume upon return that it has
*	not been deleted.
***************************************************************/

然后我们可以编写处理程序

/***************************************************************
*                      CMainFrame::OnLogMessage
* Inputs:
*       WPARAM: ignored, 0
*       LPARAM: (LPARAM)(CString *): String to log
* Result: LRESULT
*	Logically void, 0, always
* Effect:
*	Logs the message in the debug output window
* Notes:
*	The CString object must explicitly deallocated by
*	the handler for this message
***************************************************************/

(请注意,我经常在两个地方复制定义;虽然这意味着如果我进行更改,我必须更新处理程序注释,但由于我无论如何都必须编辑代码,所以这不是一个严重的危险)。

LRESULT CMainFrame::OnLogMessage(WPARAM, LPARAM lParam)
{
    CString * s = (CString *)lParam;
    c_Log.AddString(*s);
    delete s;
    return 0; // logically void, value ignored
}

跨线程发送消息

跨线程发送消息通常是个坏主意。潜在死锁的问题非常严重。如果您向一个被阻塞的线程 SendMessage,而该线程正在等待发送线程完成,您就会陷入僵局。您的进程将被阻塞,并且会一直阻塞,您将不得不引爆它才能使其消失。

请注意,您可能会无意中陷入这种情况。您绝不应该从工作线程操纵 GUI 对象,或者操纵属于发送消息的用户界面线程以外的线程拥有的 GUI 对象。如果您遇到死锁,这些是需要寻找的关键问题。

在跨线程传输信息时,最安全的方法是使用 PostMessage 来处理。跨线程的 PostMessage 不会阻塞发送方。如果您需要肯定确认,最好将您的算法重构为两阶段算法,其中一个方法向另一个线程发布消息,并期望另一个线程发布消息回来指示完成。虽然编程起来更困难,但它避免了死锁问题。

如果您必须跨线程发送,并且需要积极响应,并且存在死锁的可能性,则应使用 SendMessageTimeout。这将向线程发送消息,但如果线程在超时期限内没有响应,则消息完成,您将获得控制权,并显示错误指示。我使用的典型调用如下例所示。

// The following replaces the unconditional send
// result = wnd->SendMessage(UWM_QUERY_SOMETHING);
//
DWORD result;
if(!SendMessageTimeout(wnd->m_hWnd,      // target window
                       UWM_QUERY_SOMETHING, // message
                       0,                   // WPARAM
                       0,                   // LPARAM
                       SMTO_ABORTIFHUNG |
                       SMTO_NORMAL,
                       TIMEOUT_INTERVAL,
                       &result))
{ /* error or timed out */
    // take some appropriate action on timeout or failure
    // if we care, we can distinguish timeout from other
	// errors
    if(::GetLastError() == 0)
    { /* time out */
        // take timeout action
    } /* time out */
    else
    { /* other error */
        // take error action
    } /* other error */
} /* error or timed out */
else
{ /* successful */
    // decode the result
    switch(result)
    { /* result */
    case ...: 
        break;
    case ...:
        break;
    } /* result */
} /* successful */

跨进程发送消息

跨进程发送用户定义消息要复杂得多。首先,您必须使用注册窗口消息。使用基于 WM_APP 的消息介于严重危险和完全疯狂之间。

当您跨进程发送消息时,您实际上是在跨线程发送它。所有关于跨线程消息的注意事项都适用。但更重要的是,跨进程消息还有其他严重的限制。

最重要的是,您不能跨进程边界发送指针。这是因为进程地址空间是独立的,当在另一个进程中收到指针时,它没有意义。例如,

LRESULT CMainFrame::OnLogMessage(WPARAM, LPARAM lParam)
{
    CString * s = (CString *)lParam;
    if(s[0] == _T('$')) // app crashes hard here
    { /* special message */
        // ...
    } /* special message */
}

当执行操作 s[0] 时,接收消息的应用程序几乎肯定会发生访问故障。指针有效的可能性(如果有效,它将指向无意义的乱码)接近于零,并且指向的乱码肯定不会与 CString 相似。

您甚至不能传递共享内存的指针,即使是 DLL 共享段的共享内存。这是因为共享内存不保证在所有共享内存的进程中位于相同的位置(这在Win32 编程中详细描述)。本质上,您可以认为无法使用普通消息跨进程边界传递信息。


跨进程消息的危害

除了前面所有问题之外,跨进程发布消息还有其他危害。例如,在任何情况下,您都不能跨进程边界传递地址。

即使地址位于共享内存区域,例如共享 DLL 数据段或内存映射文件,情况也是如此。仅仅因为一个地址在一个进程中有效,并不能保证它在不同的进程中有效。如果您有证据表明情况并非如此,即您看到可以将共享内存指针从一个进程传递到另一个进程并且它有效,请注意您看到的现象是暂时的和偶然的。虽然 Win32 真诚地尝试将共享内存映射到每个进程中的相同位置,但重要的是要强调它不保证这一点。依赖这一点会使您的交付产品面临严重问题,这些问题可能几个月都不会出现。但这将在其他时候讨论。

此外,您可能会遇到讨厌的死锁情况,其中 SendMessage 没有完成,并永远挂起,或者看起来如此。

这是一个典型的例子,我们实际上在实践中见过

我的应用程序只想作为唯一副本运行。检测机制之一是搜索同一类的窗口(这是一个坏主意,因为类名是由 MFC 发明的),同一标题(这是一个坏主意,因为标题可能会根据哪个 MDI 子窗口处于活动状态而变化)等等。更好的方法之一是向每个窗口发送一个注册窗口消息并查找特定响应。以前很容易;现在有点难了。幼稚的方法是执行如下所示的操作。我在这里展示的是 EnumWindows 调用的处理程序。

BOOL CMyApp::myEnumProc(HWND hWnd, LPARAM lParam)
{
    CMyApp * me = (CMyApp *)lParam;
    if(::SendMessage(hWnd, UWM_ARE_YOU_ME))
    { /* found duplicate */
        // ... do something here, e.g.,
        me->foundPrevious = TRUE;
       return FALSE; // stop enumerating
    } /* found duplicate */
    return TRUE; // keep enumerating
}

如果我的主窗口中有一个处理程序

LRESULT CMainFrame::OnAreYouMe(WPARAM, LPARAM)
{
    return TRUE;
}

原则上,发送给任何不识别注册窗口消息的窗口的消息都会传递给 DefWindowProc,因为它不识别该消息,将返回 0,这意味着只有我的窗口会响应。这也是定位关联应用程序的客户端或服务器窗口的一种技术。

因此,如果我最初将 foundPrevious 设置为 FALSE 并调用 EnumWindows(myEnumProc, (LPARAM)this),那么当我返回时,我就知道是否找到了前一个实例。

此代码有三个主要缺陷,但都不明显。

首先,虽然这是一种识别关联窗口(例如客户端窗口或服务器窗口)的良好技术(在这种情况下,消息可能会将窗口句柄作为 WPARAM 传递,以告知关联窗口谁发送了消息),但它不适用于查找应用程序的重复实例。这是另一篇文章的主题。

第二,它可能在失败或无响应的应用程序上死锁,导致您挂起。

第三,微软费尽心思地在 Windows 98 上,作为 FrontPage 产品的一部分,安装了一个窗口,该窗口对任何消息总是返回 TRUE!!虽然很难相信有人会愚蠢到这样做,但他们确实这样做了,我们都深受其害。

让我们详细看看这些问题

第一个缺陷在您将桌面配置为单击一次启动应用程序时非常明显。习惯于双击启动应用程序的用户会双击,这将启动应用程序的两个副本。第一个副本执行 EnumWindows 循环,并看到它是唯一正在运行的实例。然后它继续创建其主窗口并继续。第二个实例出现,并执行 EnumWindows 循环。因为我们处于抢占式多线程环境中,它实际上设法在第一个应用程序仍在创建其主窗口时完成其 EnumWindows 循环!所以第一个应用程序的主窗口尚未启动,第二个应用程序找不到它,并认为它就是唯一一个应用程序。因此,我们有两个副本正在运行,这与我们想要的相反。

我们无法避免这种情况。提出任何场景(例如,在创建主窗口之前不调用 EnumWindows),您仍然会得到相同的竞态条件。

因此,我们排除了此方法用于查找应用程序的重复副本。但是,让我们看看轮询所有窗口以查找客户端或服务器的问题。在这种情况下,我可能会这样做

HWND target = ::SendMessage(hWnd, UWM_HERE_I_AM, (WPARAM)m_hWnd);

由响应处理

LRESULT CTheOtherApp::OnHereIAm(WPARAM wParam, LPARAM)
{
    other = new CWnd;
    other.Attach((HWND)wParam);
	return (LRESULT)m_hWnd;
}

这看起来像是窗口句柄的很好交换。如果您不知道 Attach 的作用,请阅读我关于 Attach/Detach 用法的文章。

现在,系统上某个地方有一个卡住的应用程序。也许它是一个 IE 副本,正在等待无限长的网页超时,并阻塞在网络处理程序中的某个地方。也许它是一个设计糟糕的程序,正在对万亿位位图进行卷积算法,并且在唯一的线程中执行,并且不允许任何消息进入(它将在大约一两周内完成)。也许它是您正在调试的程序之一,陷入循环。不管怎样。关键是,在您可能从未听说过的应用程序中,存在非常长,甚至可能是无限的延迟。您猜怎么着?您无辜的应用程序挂起了。

解决此问题的方法是使用 ::SendMessageTimeout,这样如果这些应用程序中的任何一个会阻塞您,您就不会挂起。您应该选择一个小的超时间隔,例如 100 毫秒,否则,您的启动时间会非常长。

现在谈到令人不快的部分,微软在此处确实给我们带来了麻烦。有一个应用程序,其窗口类是“Front Page Message Sink”,它表现出病态且不合群的行为,即它对收到的每条消息都返回非零值。这非常愚蠢。它没有文档,违反了所有已知的 Windows 标准,并且对您来说是致命的。这只会在您运行个人 Web 服务器时出现。但是,这样做是绝对不可原谅的。

我只知道它总是对它收到的任何类型的注册窗口消息返回 1。也许这是它返回的唯一值。我不知道,微软的任何人都没有回应我的错误报告,也没有解释到底发生了什么。

因此,将 ::SendMessage 的结果或 ::SendMessageTimeoutDWORD 结果与 0 进行比较是没有意义的。到目前为止,如果您看到 0 或 1,您无法依赖该值具有任何意义。如果运气好的话,Microsoft 会修复此问题(哈!),理想情况下,他们将来不会引入其他此类疯狂的无故示例。

我通过修改接收处理程序返回一个非零、非一的值来解决这个问题。例如,我实际上使用 ::RegisterWindowMessage 来获取一个值,尽管我想我也可以使用 ::GlobalAddAtom。至少我知道 ::RegisterWindowMessage 会返回一个非零、非一的值。

// ... in a header file shared by both applications
#define MY_TOKEN T("Self token-{E32F4800-8180-11d3-BC36-006067709674}")

// ... rewrite the handler as
LRESULT CMainFrame::OnAreYouMe(WPARAM, LPARAM)
{
    return ::RegisterWindowMessage(MY_TOKEN);
}

// In the calling module, do something like
UINT token = ::RegisterWindowmessage(MY_TOKEN);

// ... and recode the caller, the EnumWndProc, as
DWORD result;
BOOL ok = ::SendMessageTimeout(hWnd,
                               (WPARAM)m_hWnd,   // WPARAM
                               0,                // LPARAM
                               SMTO_ABORTIFHUNG |
                               SMTO_NORMAL,
                               TIMEOUT_INTERVAL,
                               &result));

if(!ok)
    return TRUE; // failed, keep iterating
if(result == token)
{ /* found it */
    // ... do whatever you want here
    return FALSE; // done iterating
} /* found it */

如果您尝试返回一个句柄,而不仅仅是一个指定值,您必须进行测试

if(result != 0 && result != 1)    // avoid Microsoft problem
{ /* found it */
    target = new CWnd;
    target->Attach(hWnd);
    // ... do whatever you want here
    return FALSE; // done iterating
} /* found it */

请注意,如果您选择使用 ::FindWindow,它会在内部执行 ::EnumWindows,因此可能会受到相同的永久挂起的影响。使用 ::FindWindow 非常危险,因为它假设实际上每个 ::SendMessage 都会完成。请注意,::FindWindow 会执行 ::SendMessage(hWnd, WM_GETTEXT,...) 来获取要搜索的窗口文本。


WM_COPYDATA

有一个可能有用的消息 WM_COPYDATA,它跨越边界传输信息。它通过内核传递数据来实现。内核在接收进程中分配空间来保存从源进程复制到目标进程的信息。或者类似的东西。实现细节实际上对您是隐藏的。

发送方传递指向 COPYDATASTRUCT 的指针,该结构定义如下

typedef struct tagCOPYDATASTRUCT {
    DWORD dwData;
    DWORD cbData;
    PVOID lpData;
} COPYDATASTRUCT, *PCOPYDATASTRUCT;

dwData 成员保存着正在传递给目标进程的任意 32 位值。您可以将其设置为两个进程同意的任何值。cbData 成员表示 lpData 引用的值中有多少字节。当目标进程收到信息时,它通过方法处理它

BOOL CMainFrame::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct)

CWnd * 是对发送窗口的引用,而 COPYDATASTRUCT * 引用传入的 COPYDATASTRUCT。请注意,您不知道发送方中的实际地址。传入的数据不得包含指针。

WM_COPYDATA 存在一些潜在问题,您需要识别是谁发送了它才能判断它是否有效,或者您必须有其他方法来识别它。一种处理方法是使用我们的老朋友 GUID。如果您将 GUID 放在数据包的开头,您可以将其与预期的 GUID 进行比较,如果它们相等,您就可以确定收到的数据包是您想要的数据包。

您不得存储 pCopyDataStruct.lpData 指针,因为在从 OnCopyData 处理程序返回后,该指针应被视为不再有效。您也绝不能尝试写入 lpData 指针引用的数据;它必须被视为只读。


向视图发送消息

MFC 有一个出色的消息路由机制。命令消息首先到达活动视图,然后是视图框架,然后是文档,依此类推。这适用于 WM_COMMANDWM_UPDATE_COMMAND_UI 消息,但不适用于任何其他类型的消息,包括用户定义消息。这非常麻烦,而且有点愚蠢,因为微软很容易实现。因此,当您需要向某个子窗口发送消息,但由于您没有直接响应 GUI 组件而无法发送 WM_COMMAND 消息时,就会出现问题。

当然,您可以发明一个虚构的控件并生成自己的模仿 WM_COMMAND 消息。但这非常危险,并且会严重导致未来的不可维护性。

你能做什么?

我做了几件事。有时,我只是在子框架中放置一个消息处理程序,其唯一目的是将消息路由到框架中包含的子窗口。这可能很麻烦,但可以让您严格控制消息的处理方式。因此,主框架(我向其发布消息,通常来自一个线程)具有以下形式的处理程序:

// ... in the message map
ON_REGISTERED_MESSAGE(UWM_THREAD_DID_SOMETHING, OnThreadDidSomething)

// ... the handler
LRESULT CMainFrame::OnThreadDidSomething(WPARAM wParam, LPARAM lParam)
{
    CView * active = GetActiveView();
    if(active != NULL)
        active->SendMessage(UWM_THREAD_DID_SOMETHING, wParam, lParam);
    return 0;
}

有时,您必须将消息发送到视图的父框架窗口。这通过以下代码完成

RESULT CMainFrame::OnThreadDidSomething(WPARAM wParam, LPARAM lParam)
{
    CFrameWnd * active = GetActiveFrame();
    if(active != this)
        active->SendMessage(UWM_THREAD_DID_SOMETHING, wParam, lParam);
    return 0;
}

注意上面的特殊测试。如果由于某种原因,您在线程仍在运行时设法杀死了所有 MDI 子项,GetActiveFrame 将返回 this,这意味着您将进入一个半无限的 SendMessage 循环,该循环将在堆栈空间不足时终止。请注意,由于函数定义为返回 this,我们不必担心返回临时窗口句柄的可能性,这是我在关于 Attach/Detach 用法的文章中讨论的一个注意事项。这要求您使用 CMDIChildFrame 子类,并在其中引入类似的处理程序

BEGIN_MESSAGE_MAP(CMyMDIChildFrame, CMDIChildWnd)
    ON_REGISTERED_MESSAGE(UWM_THREAD_DID_SOMETHING, OnThreadDidSomething)

// and the handler is
LRESULT CMyMDIChildFrame::OnThreadDidSomething(WPARAM wParam, LPARAM lParam)
{
    // ... do things to the frame window (resize, reposition?)
    // and if appropriate, pass the message down...
    SendMessageToDescendants(UWM_THREAD_DID_SOMETHING,
	                         wParam,
	                         lParam,
	                         FALSE, // only go one level deep
                             TRUE); // only to permanent windows
    return 0;
}

现在,您可能会想知道我为什么不直接将其发送给子窗口。部分原因是我很懒,SendMessageToDescendants 可以满足我的需求。部分原因是我不需要结果,因为它来自 PostMessage。在需要将消息路由到需要结果的后代的情况下,我将改为编写

CView * view = (CView *)GetWindow(GW_CHILD);
ASSERT(view != NULL && view->IsKindOf(RUNTIME_CLASS(CView)));
view->SendMessage(...);

摘要

用户定义消息是一种强大而灵活的机制,用于处理应用程序各层之间、应用程序线程之间以及进程之间信息和控制的传递。但是,有效地使用它们需要一定程度的谨慎。我倾向于使用 ::RegisterWindowMessage 和 GUID 来定义所有消息。线程间和进程间发送是危险的,应避免;如果必须使用,请使用 SendMessageTimeout 以确保您不会陷入死锁。


这些文章中表达的观点是作者的观点,不代表,也不被微软认可。

发送邮件至newcomer@flounder.com提出关于本文的问题或评论。
版权所有 © 1999CompanyLongName保留所有权利。
http://www.flounder.com/mvp_tips.htm
© . All rights reserved.