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



我有一个代码库,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 typedef:

ON_COMMAND(CID_ButtonAction, &ThisClass::OnButtonAction)

到目前为止还不错。。。唯一的问题是,我在许多单独的文件中有数百个这样的消息映射条目。有什么工具可以解决这个问题吗?一些晦涩难懂的Visual Studio魔术?或者可以在这里通过regex使用替换吗?

最后,我提出了一个从MinGW:运行的sed命令

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

解释它的作用:

  • -b将文件视为二进制文件(可选,以在Windows中保持行尾)*
  • -re支持扩展正则表达式
  • -i原位置换
  • /^BEGIN_MESSAGE_MAP/,/^END_MESSAGE_MAP/仅匹配这两个字符串之间的文本
  • /!s替换命令,它将忽略之前匹配的任何内容
  • /(BEGIN_MESSAGE_MAP|//)/匹配要忽略的行开始(消息映射的第一行或注释掉的行)
  • /(.*),s{0,}/1, &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个文件,我只需要在已经使用类方法成员表示法的地方进行两次手动调整。sed命令可以根据这一点进行调整(例如,检查行中最后一个逗号后面是否跟有&),但这对我来说已经足够了。

编辑-添加了-b选项。这将文件视为二进制文件。在Windows上,这可以防止用Unix字符替换原始换行符-如果不启用此选项,任何处理过的文件的git diff都将看起来像是整个文件被删除并再次添加。

错误消息有点奇怪,我认为这与Visual Studio和CLANG在处理源代码方面的差异有关。

我手头的编译器是Visual Studio 2005,我有一个MFC应用程序,所以Visual Studio 2005的MFC源代码很方便。我用同样的解决方案快速浏览了一下Visual Studio 2015,发现MFC头文件似乎相似。所以我将以VisualStudio2005MFC为基础。

位于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);

类CCD_ 14是从其派生诸如CWndCWinThread之类的其它类以及使用消息映射的其它MFC类的基类。

因此,ON_COMMAND()宏使用static_cast<>作为窗口或线程目标的基类。也许其他更了解编译器的人可以提供一个实际的解释,说明编译器在做什么,以及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()宏的当前定义,以便您可以用自己的定义替换它。

其次,它对方法指针使用类方法成员关系表示法。我无法用CLANG进行测试,但它应该做与您手工做的相同的源文本替换,您认为这是有效的。

ON_COMMAND(CID_ButtonAction, &SomeForm::OnButtonAction)

ThisClassBEGIN_MESSAGE_MAP()指令(例如BEGIN_MESSAGE_MAP(CFrameworkWnd, CWin))中指定的类的typedef,由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[] =  
{

我在VisualStudio中测试了这种方法,一切都编译得很好,它可以在VisualStudio2005中使用。

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

奇怪的差异

考虑到这一点,afxmsg_.h中各种宏的一个奇怪的区别是使用类方法指针表示法的一整套宏。示例如下:

#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)

合计

使用这种用自己的版本覆盖默认宏的方法,include文件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至少可以从VisualStudio6.x开始,而且有机会使宏统一。例如,VisualStudio2005的发布将是一个好时机。也许有人担心与巨大的VisualStudio6.x MFC代码库的向后兼容性?

现在我知道为什么要定制特定的static_cast<>。它允许检测具有错误或不匹配接口签名的类方法以及编译错误。因此,C风格的转换是为了纠正AFX_MSGMAP_ENTRY中函数指针的定义,而static_cast<>是为了在方法接口与预期接口不同的情况下,通过发出编译器错误来捕捉由于接口缺陷而导致的程序员错误。

最新更新