重构 MFC 消息映射以包含完全限定的成员函数指针

Refactor MFC message maps to include fully qualified member function pointers

我有一个代码库,其中 MFC 消息映射以这种形式编写:

BEGIN_MESSAGE_MAP(SomeForm, BaseForm)
    ON_COMMAND(CID_ButtonAction, OnButtonAction)
END_MESSAGE_MAP()

这在 MSVC 中编译得很好。当我想在 Clang 中编译相同的代码时,出现 call to non-static member function without an object argument 错误,因为 OnButtonAction 不是指定成员函数指针的正确形式。代码很容易修复:

    ON_COMMAND(CID_ButtonAction, &SomeForm::OnButtonAction)

或者我们可以使用 BEGIN_MESSAGE_MAP() 宏中的 ThisClass 类型定义:

    ON_COMMAND(CID_ButtonAction, &ThisClass::OnButtonAction)

到目前为止一切顺利...唯一的问题是我在许多单独的文件中有数百个这样的消息映射条目。有什么工具可以解决这个问题吗?一些晦涩难懂的 Visual Studio 魔法?还是可以在这里通过正则表达式使用替换?

错误消息有点奇怪,我想这与 Visual Studio 和 CLANG 之间在处理源方面的差异有关。

我手边的编译器是 Visual Studio 2005,我有一个正在开发的 MFC 应用程序,所以 Visual Studio 2005 的 MFC 源很方便。我用相同的解决方案快速浏览了 Visual Studio 2015,看起来 MFC 头文件相似。所以我将基于 Visual Studio 2005 MFC。

位于afxmsg_.h的ON_COMMAND()宏定义如下:

#define ON_COMMAND(id, memberFxn) \
    { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSigCmd_v, \
        static_cast<AFX_PMSG> (memberFxn) },
        // ON_COMMAND(id, OnBar) is the same as
        //   ON_CONTROL(0, id, OnBar) or ON_BN_CLICKED(0, id, OnBar)

AFX_PMSG在文件afxwin.h中定义为:

// pointer to afx_msg member function
#ifndef AFX_MSG_CALL
#define AFX_MSG_CALL
#endif
typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);

class CCmdTarget 是一个基础 class 派生出其他 class ,例如 CWndCWinThread 以及其他使用消息映射的 MFC classes。

所以 ON_COMMAND() 宏正在使用 static_cast<> 来作为 window 或线程目标的基础 class。也许知识渊博的其他人可以提供有关编译器正在做什么以及 C++ 语言规范如何处理此构造的实际解释。

然而,从更实际的角度来看,我建议您编写自己版本的 ON_COMMAND() 宏,并将此版本插入解决方案每个项目中的 StdAfx.h 文件中.我选择了 StdAfx.h 文件,因为每个项目只有一个,它是一个中心点,单个修改可以影响多个编译单元。

在文件的底部,在所有各种包含之后和关闭已包含的头文件测试的 #endif 之前,添加以下源代码行。

#undef ON_COMMAND

#define ON_COMMAND(id, memberFxn) \
    { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSigCmd_v, \
        static_cast<AFX_PMSG> (&ThisClass :: memberFxn) },
        // ON_COMMAND(id, OnBar) is the same as
        //   ON_CONTROL(0, id, OnBar) or ON_BN_CLICKED(0, id, OnBar)

这有两个作用。

首先,它取消定义 ON_COMMAND() 宏的当前定义,以便您可以用自己的定义替换它。

其次,它对方法指针使用class方法成员表示法。我无法使用 CLANG 进行测试,但是它应该进行与您所说的手动操作相同的源文本替换。

ON_COMMAND(CID_ButtonAction, &SomeForm::OnButtonAction)

ThisClassBEGIN_MESSAGE_MAP() 指令(例如 BEGIN_MESSAGE_MAP(CFrameworkWnd, CWin))中指定的 class 的类型定义,由 BEGIN_MESSAGE_MAP() 宏生成,看起来喜欢:

#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
    PTM_WARNING_DISABLE \
    const AFX_MSGMAP* theClass::GetMessageMap() const \
        { return GetThisMessageMap(); } \
    const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() \
    { \
        typedef theClass ThisClass;                        \
        typedef baseClass TheBaseClass;                    \
        static const AFX_MSGMAP_ENTRY _messageEntries[] =  \
        {

我用 Visual Studio 测试了这种方法,一切都编译得很好,它适用于 Visual Studio 2005。

请注意,可能还有其他消息映射宏可能需要类似的解决方法,因为 static_cast<AFX_PMSG> 在大多数消息映射宏中似乎很常见。

奇怪的区别

仔细研究一下,afxmsg_.h 中各种宏的一个奇怪区别是使用 class 方法指针表示法的一整套宏。示例如下:

#define ON_WM_PAINT() \
    { WM_PAINT, 0, 0, 0, AfxSig_vv, \
        (AFX_PMSG)(AFX_PMSGW) \
        (static_cast< void (AFX_MSG_CALL CWnd::*)(void) > ( &ThisClass :: OnPaint)) },

查看一些特定的事件宏,它们似乎重用了 ON_CONTROL() 宏,因此除了 ON_COMMAND() 宏之外还替换该宏会波及控制特定 MFC 的集合宏。

// Combo Box Notification Codes
#define ON_CBN_ERRSPACE(id, memberFxn) \
    ON_CONTROL(CBN_ERRSPACE, id, memberFxn)

求和

使用这种用您自己的版本覆盖默认宏的方法,似乎包含文件 afxmsg_.h 包含需要更改的内容的列表。似乎还有两组 MFC 宏需要替换版本,一组靠近文件顶部(以 ON_COMMAND() 开头),另一组靠近包含文件底部 afxmsg_.h .

例如 ON_MESSAGE() 宏需要更改为:

// for Windows messages
#define ON_MESSAGE(message, memberFxn) \
    { message, 0, 0, 0, AfxSig_lwl, \
        (AFX_PMSG)(AFX_PMSGW) \
        (static_cast< LRESULT (AFX_MSG_CALL CWnd::*)(WPARAM, LPARAM) > \
        (&ThisClass :: memberFxn)) },

我想知道为什么会出现混合样式(可能是因为这些年来不同的人添加了新的宏而不费心去更改现有的?)。我很好奇为什么在过去二十年的某个时候没有解决这个问题,因为 MFC 至少可以追溯到 Visual Studio 6.x 并且本来有机会使宏统一。例如 Visual Studio 2005 年的发布就是个好时机。也许有人担心与庞大的 Visual Studio 6.x MFC 代码库的向后兼容性?

现在我知道为什么要量身定做,具体 static_cast<>。它允许检测带有编译错误的错误或不匹配接口签名的 class 方法。因此,C 风格的强制转换是通过 AFX_MSGMAP_ENTRY 中函数指针的定义来纠正错误,而 static_cast<> 是通过在方法接口出现时发出编译器错误来捕获由于接口缺陷引起的程序员错误与预期不同。

最后我从 MinGW 运行 想出了一个 sed 命令:

sed -b -i -re '/^BEGIN_MESSAGE_MAP/,/^END_MESSAGE_MAP/{/(BEGIN_MESSAGE_MAP|\/\/)/!s/(.*),\s{0,}/, \&ThisClass::/;}' *.cpp

解释它的作用:

  • -b 将文件视为二进制文件(可选,以保持行尾在 Windows)*
  • -re支持扩展正则表达式
  • -i 就地替换
  • /^BEGIN_MESSAGE_MAP/,/^END_MESSAGE_MAP/ 只匹配这两个字符串之间的文本
  • /!s 替换命令将忽略您之前匹配的任何内容
  • /\(BEGIN_MESSAGE_MAP\|\/\/\)/ 匹配要忽略的行开头(消息映射的第一行或注释掉的行)
  • /(.*),\s{0,}/, \&ThisClass::/, &ThisClass::
  • 替换一行中的最后一个逗号,后跟 0+ 个空格

示例输入:

BEGIN_MESSAGE_MAP(SomeForm, BaseForm)
    ON_COMMAND(CID_ButtonAction, OnButtonAction)
    ON_NOTIFY_EX(CID_Notify, 0, OnNotify)
END_MESSAGE_MAP()

输出:

BEGIN_MESSAGE_MAP(SomeForm, BaseForm)
    ON_COMMAND(CID_ButtonAction, &ThisClass::OnButtonAction)
    ON_NOTIFY_EX(CID_Notify, 0, &ThisClass::OnNotify)
END_MESSAGE_MAP()

这很好用,对于大约 500 个文件,我只需要在已经使用 class 方法成员表示法的地方进行两次手动调整。可以调整 sed 命令以解决这个问题(例如,检查该行的最后一个逗号是否后跟 &),但这对我的目的来说已经足够了。

EDIT - 添加了 -b 选项。这将文件视为二进制文件。在 Windows 上,这会阻止用 Unix 换行符替换原始换行符 - 如果不启用此选项,任何已处理文件的 git 差异看起来就像整个文件已被删除并重新添加。