MFC的一个bug,没耐心看,就看看结论好了
发信站: 饮水思源 (2003年10月25日21:20:37 星期六), 站内信件
MFC的一个bug
1,问题的提出
(1)Dll的创建
是这样的,手头上有一个ActiveX,它通过一些列的方法实现了一定的功能,然而
接口函数特别多。为了完成一个特定的功能,也许要进行一系
列的方法调用,这特别讨厌。
为了解决这个问题,想了一个方法,就是用一个动态库封装这个ActiveX,这个动
态库导出三个函数,一个是初始化(InitializeDll),另一个
是功能函数(Function),再一个是反初始化函数(UninitializeDll)。
为了封装这个ActiveX,简单的方法就是在这个dll中必须创建一个ActiveX控件,
大家知道,创建一个ActiveX的简单方法,就是在MFC生成的代
码中,通过VC插入一个这个控件,并且新创建一个对话框,然后在对话框资源模板
里面,插入一个这个控件就好了。
haha,我就是这么做的。
于是,我用Appwizard生成了一个MFC Regular Dll,然后在资源中插入了一个对话
框,并且插入了一个控件(为了试验起见,就用WebBrowser2
控件了),然后在InitializeDll函数中,创建这个对话框就可以了,对了,这个
时候的对话框,最好是无模态对话框,我使用了
CDialog::Create来创建非模态对话框。
这样,这个初始化函数就这样了:
extern "C" void WINAPI InitializeDll()
{
static CTestDialog dlg;
dlg.Create();
}
其中,
BOOL CTestDialog::Create()
{
return CDialog::Create(IDD);
}
然后,我们就认为,在初始化函数中,这个对话框,以及其中的控件,都应该创建
成功了,就可以了。
解下来的工作,应该是创建一个测试器,来看看这些代码有没有工作。
(2)Tester的编写
写这种Tester,我喜欢用VC里面所谓的“基于对话框”的程序来做,用Appwizard
生成一个基于对话框的程序,很好,自动在对话框资源里面加
入了两个按钮,我的习惯是处理OK按钮的事件,这样子,这个函数就这样了:
void CTesterDlg::OnOk()
{
// load library
HINSTANCE hInst = ::LoadLibrary(_T("Test.dll"));
if (hInst != NULL)
{
// get func
typedef void (WINAPI* InitFunc)();
InitFunc init = (InitFunc)::GetProcAddress(hInst,
_T("InitializeDll"));
if (init != NULL)
init();
//::FreeLibrary(hInst);
}
}
hehe,简单点,先不释放动态库(否则,如果动态库成功创建了,后面你又把动态
库释放了,是要出现错误的,因此,暂时这么着吧)。
好了,可以测试运行了,
(3)测试
运行程序,你发现,那个dll里面的对话框没有显示出来!在确信动态库的
InitializeDll()函数被调用了之后,哦,可能是dll里面的那个对话
框资源的Visible属性没有设置,然后回头设置一下,发现这个对话框,还是没有
显示,难道对话框创建不成功???
有可能哦。
记得对话框里面需要插入ActiveX的时候,需要预先调用一条
AfxEnableControlContainer函数,好像是这样的,查查MSDN,的确是这样的,于
是,不客气,在对话框的InitInstance中加入这条语句,这样子,函数成为这样了
:
BOOL CTestApp::InitInstance()
{
::AfxEnableControlContainer();
return CWinApp::InitInstance();
}
这里,放心好了,CTestApp()是在DllMain中调用的,肯定在InitializeDll()之前
调用,因此,调用顺序上没有问题。
再次运行,现象依旧!!!问题麻烦了。看来,很多自以为是的东西,其实不然啊
。
2,调试
(1)首先怀疑这个控件是否有问题
(当然我们知道WebBrowser2控件是没有问题的,不过,如果这个控件不是你熟悉
的空间呢???)
这个好办,我就在我的测试器(Tester)的对话框中插入这个控件,然后再次运行,
看看测试器的对话框是否能成功创建。
测试的结果显示:
乖乖!不但我的测试器对话框成功显示了,而且,那个Dll中的对话框也成功显示
了,包括其中的控件啊!!!一个令人振奋的消息!
然后我把测试器对话框中的WebBrowser2控件去掉,可怜得很,那个dll中的对话框
随之又创建不成功了。
顺便把Dll中的对话框里面的WebBrowser控件去掉,发现这个对话框又能创建了,
可见问题就是出现在动态库的对话框中使用了ActiveX控件导
致的。而ActiveX是没有问题的!
(2)只好跟踪MFC了:首次跟踪
实在是没有办法的办法了。
在dll创建对话框的地方设置一个断点吧,开始逐条语句跟踪,一个函数一个函数
跟踪,哇,很辛苦的,调入堆栈很深的,没有发现什么意外啊
,不过,到是发现了不少有趣的事情:
——MFC在创建窗口的时候,为了维护CWnd和HWND的对应关系,使用了Windows的钩
子技术(HOOK),设置了一个WH_CBT钩子,以监视创建窗口的
消息,并且以此来维护CWnd和HWND的对应关系;
——MFC在创建对话框的时候,首先要检测其中是否包含了ActiveX控件,如果包含
了,还要单独列出来,先创建不包含ActiveX的部分,接着创
建ActiveX控件;
——如果对话框包括了ActiveX,那么就要创建一个ActiveX Container,这也是了
,因为需要一个Container Site,这些东东MFC为我们创建了
,的确方便;涉及到一个类COleControlContainer;还有一个类,怪怪的,
COccManager;
——等等吧,还有一些,不详细列举了;
——还有一个关键的东西,呵呵,就是MFC接管了WM_INITDIALOG消息,并且在其中
来创建对话框中的ActiveX控件,这个是非常重要的线索啊!
疑问:那么我们用ClassWizard不是可以加入消息WM_INITDIALOG的消息响应函数么
?那这条消息到底是MFC还是我们的函数在处理?看看MFC为
我们生成的这条消息的处理函数吧,其实,它不是个消息处理函数,只是一个虚函
数罢了!如下:
函数的声明(Prototype):
virtual BOOL OnInitDialog();
注意,函数的声明前面,也没有消息前缀:afx_msg,察看消息映射宏,你找不到
类似下面的消息映射:
ON_WM_INITDIALOG()
注意,没有这样的消息映射,看看msdn中,你发现,其实它只是一个虚函数!只是
该虚函数在MFC的WM_INITDIALOG消息处理中被调用了,这就是
全部。
(3)再次跟踪
再次跟踪,注意,发现问题了,进入到了一个关键的MFC函数:
BOOL CWnd::CreateDlgIndirect(LPCDLGTEMPLATE lpDialogTemplate, CWnd*
pParentWnd, HINSTANCE hInst)
这个函数作了好多事情哦,包括创建对话框的预处理,借用MFC代码中的注释来说
明吧(以下代码摘自MFC):
#ifndef _AFX_NO_OCC_SUPPORT
// separately create OLE controls in the dialog template
if (pOccManager != NULL)
{
if (!SetOccDialogInfo(&occDialogInfo))
return FALSE;
lpDialogTemplate = pOccManager->PreCreateDialog(&occDialogInfo,
lpDialogTemplate);
}
if (lpDialogTemplate == NULL)
return FALSE;
#endif //!_AFX_NO_OCC_SUPPORT
翻译出来,大概是说,对话框模板中的OLE控件(们,哈哈)要分离出来然后创建
吧。大概PreCreateDialog这个函数就是做这个事情的,跟踪
进去,发现的确是这样的,其中调用了一个函数,如下:
const DLGTEMPLATE* COccManager::PreCreateDialog(_AFX_OCC_DIALOG_INFO*
pDlgInfo,
const DLGTEMPLATE* pOrigTemplate)
{
ASSERT(pDlgInfo != NULL);
pDlgInfo->m_ppOleDlgItems =
(DLGITEMTEMPLATE**)malloc(sizeof(DLGITEMTEMPLATE*) *
(DlgTemplateItemCount(pOrigTemplate) + 1));
if (pDlgInfo->m_ppOleDlgItems == NULL)
return NULL;
DLGTEMPLATE* pNewTemplate = SplitDialogTemplate(pOrigTemplate,
pDlgInfo->m_ppOleDlgItems);
pDlgInfo->m_pNewTemplate = pNewTemplate;
return (pNewTemplate != NULL) ? pNewTemplate : pOrigTemplate;
}
其中一个函数名就是了:SplitDialogTemplate,翻译出来,莫非:分割对话框模
板???
分割完了,该处理字体了,借用MFC代码如下:
// On DBCS systems, also change "MS Sans Serif" or "Helv" to system
font.
if ((!bSetSysFont) && GetSystemMetrics(SM_DBCSENABLED))
{
bSetSysFont = (strFace == _T("MS Shell Dlg") ||
strFace == _T("MS Sans Serif") || strFace == _T("Helv"));
if (bSetSysFont && (wSize == 8))
wSize = 0;
}
if (bSetSysFont)
{
CDialogTemplate dlgTemp(lpDialogTemplate);
dlgTemp.SetSystemFont(wSize);
hTemplate = dlgTemp.Detach();
}
自己翻译吧。
接着开始设置钩子了,并且开始创建对话框:
// create modeless dialog
AfxHookWindowCreate(this);
hWnd = ::CreateDialogIndirect(hInst, lpDialogTemplate,
pParentWnd->GetSafeHwnd(), AfxDlgProc);
执行到这里,好像还是正常啊,那么对话框是否创建成功了呢?接着跟踪:
接着这样的代码,也没有问题的:
#ifndef _AFX_NO_OCC_SUPPORT
if (pOccManager != NULL)
{
pOccManager->PostCreateDialog(&occDialogInfo);
if (hWnd != NULL)
SetOccDialogInfo(NULL);
}
#endif //!_AFX_NO_OCC_SUPPORT
if (!AfxUnhookWindowCreate())
PostNcDestroy(); // cleanup if Create fails too soon
这里看不出有什么问题,接着,问题出现了:
// handle EndDialog calls during OnInitDialog
if (hWnd != NULL && !(m_nFlags & WF_CONTINUEMODAL))
{
::DestroyWindow(hWnd);
hWnd = NULL;
}
到这个if这条语句,发现,hwnd的确不是NULL的,怎么?m_nFlags=0x100,而
WF_CONTINUEMODAL=0x10,导致这个判断的结果为TRUE,从而导致
执行了括号中的内容了,而这个内容就是销毁窗口啊。不是吧,我们是在创建窗口
啊,怎么销毁窗口了?看看m_nFlags的值吧,怎么其中没有
WF_CONTINUEMODAL呢?察看前面的代码,发现在设置钩子前面,有这样的语句:
// setup for modal loop and creation
m_nModalResult = -1;
m_nFlags |= WF_CONTINUEMODAL;
这不是使得m_nFlags中包含了WF_CONTINUEMODAL么?怎么执行到后面就没有了?难
道在创建窗口的时候,m_nFlags的值被改变了???
(4)m_nFlags
该查查这个值在哪个函数里面改变了,这个工作,好办,呵呵,我习惯了使用VC的
Find In Files功能了,在MFC的source code中查找这个字符
串,发现总共55行代码中出现了m_nFlags,然后使用VC的Find功能,在output窗口
里面查找WF_CONTINUEMODAL,定位在
D:\MSVS\VC98\MFC\SRC\WINCORE.CPP(3526): m_nFlags &= ~WF_CONTINUEMODAL;
很有意思啊,这个cpp文件是WinCore,这一行语句是去掉WF_CONTINUEMODAL的,为
什么?定位看看,这个函数是这样的:
void CWnd::EndModalLoop(int nResult)
{
ASSERT(::IsWindow(m_hWnd));
// this result will be returned from CWnd::RunModalLoop
m_nModalResult = nResult;
// make sure a message goes through to exit the modal loop
if (m_nFlags & WF_CONTINUEMODAL)
{
m_nFlags &= ~WF_CONTINUEMODAL;
PostMessage(WM_NULL);
}
}
原来这样子啊,好办,在这个函数中设置一个断点,然后断点处停下的时候,察看
堆栈,发现堆栈如下:
...
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT*
pResult)
LRESULT CDialog::HandleInitDialog(WPARAM, LPARAM)
void CDialog::EndDialog(int nResult)
其中,调用了EndModalLoop这个函数!!!
(5)HandleInitDialog
EndDialog这个函数没什么,关键的问题是,HandleInitDialog这个函数为什么会
调用EndDialog。
HandleInitDialog?莫非就是对话框的WM_INITDIALOG消息处理函数?不管是不是,
反正是起了这个作用了!
看看代码吧:
#ifndef _AFX_NO_OCC_SUPPORT
// create OLE controls
COccManager* pOccManager = afxOccManager;
if ((pOccManager != NULL) && (m_pOccDialogInfo != NULL))
{
BOOL bDlgInit;
if (m_lpDialogInit != NULL)
bDlgInit = pOccManager->CreateDlgControls(this, m_lpDialogInit,
m_pOccDialogInfo);
else
bDlgInit = pOccManager->CreateDlgControls(this,
m_lpszTemplateName,
m_pOccDialogInfo);
if (!bDlgInit)
{
TRACE0("Warning: CreateDlgControls failed during dialog
init.\n");
EndDialog(-1);
return FALSE;
}
}
#endif
注意其中的EndDialog,前面是CreateDlgControls,莫非就是创建对话框中的控件
?难道创建WebBrowser2控件不成功?跟踪
CreateDlgControls函数,看怎么回事,这样就到了下面这个函数:
BOOL COccManager::CreateDlgControls(CWnd* pWndParent, void* lpResource,
_AFX_OCC_DIALOG_INFO* pOccDlgInfo)
这个函数也没什么,其中调用了如下的函数:
// Create the OLE control now.
hwNew = CreateDlgControl(pWndParent, hwAfter, bDialogEx,
pDlgItem, nMsg, (BYTE*)lpnRes, dwLen);
两个函数名,好像就差一个s,注意,这里就是创建OLE控件了,WebBrowser控件,就
应该是这个函数创建吧!跟踪进去,看看,hoho,有了:
if (SUCCEEDED(hr) &&
pWndParent->InitControlContainer() &&
pWndParent->m_pCtrlCont->CreateControl(NULL, clsid, NULL,
pItem->style,
rect, pItem->id, pMemFile, (nMsg == WM_OCC_LOADFROMSTORAGE),
bstrLicKey, &pSite))
{
...
}
InitControlContainer(),这个函数跟踪进去,其实没什么啦,关键就是后面的哪
个函数,再次跟踪进去,就到了
BOOL COleControlContainer::CreateControl( CWnd* pWndCtrl, REFCLSID
clsid,
LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, UINT nID,
CFile* pPersist, BOOL bStorage, BSTR bstrLicKey,
COleControlSite** ppNewSite )
哦,到了COleControlcontainer这个类了,这个函数本身也简单,调用了另一个成
员函数,CreateControl,就是C++中的,重载?多态?分不
清楚,反正就是函数的参数类型,参数数目不同,函数名字相同的那种,到了下面
的函数:
BOOL COleControlContainer::CreateControl(CWnd* pWndCtrl, REFCLSID
clsid,
LPCTSTR lpszWindowName, DWORD dwStyle, const POINT* ppt, const SIZE*
psize,
UINT nID, CFile* pPersist, BOOL bStorage, BSTR bstrLicKey,
COleControlSite** ppNewSite)
{
COleControlSite* pSite = NULL;
TRY
{
pSite = afxOccManager->CreateSite(this);
}
END_TRY
if (pSite == NULL)
return FALSE;
BOOL bCreated = SUCCEEDED( pSite->CreateControl(pWndCtrl, clsid,
lpszWindowName, dwStyle, ppt, psize, nID, pPersist, bStorage,
bstrLicKey ) );
if (bCreated)
{
ASSERT(pSite->m_hWnd != NULL);
m_siteMap.SetAt(pSite->m_hWnd, pSite);
if (ppNewSite != NULL)
*ppNewSite = pSite;
}
else
{
delete pSite;
}
return bCreated;
}
这里首先准备一个COleControlSite,然后通过这个Site对象来创建控件,
CreateSite函数很简单,就是new了一个对象,就看pSite->CreateControl了(原来
,创建一个OLE控件这么麻烦!),顺次到了如下的函数:
HRESULT COleControlSite::CreateControl(CWnd* pWndCtrl, REFCLSID clsid,
LPCTSTR lpszWindowName, DWORD dwStyle, const POINT* ppt, const SIZE*
psize,
UINT nID, CFile* pPersist, BOOL bStorage, BSTR bstrLicKey)
{
...
// Initialize OLE, if necessary
_AFX_THREAD_STATE* pState = AfxGetThreadState();
if (!pState->m_bNeedTerm && !AfxOleInit())
return hr;
if (SUCCEEDED(hr = CreateOrLoad(clsid, pPersist, bStorage,
bstrLicKey)))
{
...
}
if (SUCCEEDED(hr))
{
...
}
return hr;
}
我把代码都删掉了,否则没法看,太长了。
这样看来,就简单了,因为OLE控件,涉及到OLE,就必须初始化OLE,所以,函数
调用了AfxOleInit来初始化OLE(注意,ActiveX本身就是基于
COM的,使用了Automation吧,是OLE对象的一种),
代码执行还好,接着,就执行CreateOrLoad函数了,这个函数执行就不对了!执行
结果,hr = 0x800401f0,这个值是不能让人接受的,使用
ErrorLookup吧,看看这个值,好像不是那么特殊的值啊,怎么意思很特殊呢:
尚未调用 CoInitialize。
不会吧!我们不是调用了AfxOleInit么?难道这个函数没起作用???
(6)AfxOleInit
这个函数到底怎么工作啊,再次使用Find In Files功能,看看这个函数:
BOOL AFXAPI AfxOleInit()
{
...
if (afxContextIsDLL)
{
pState->m_bNeedTerm = -1; // -1 is a special flag
return TRUE;
}
// first, initialize OLE
SCODE sc = ::OleInitialize(NULL);
...
}
哦,问题原来就在这里啊!!!
看看吧,afxContextIsDll表示当前这个模块是否dll,显然,我们讨论的就是dll
,所以这个值是true,自然就执行括号中的语句了,那就是直
接返回true,搞错啊,没进行初始化就返回成功了!后面的OleInitialize根本就
没有执行啊!
(7)修正后再次测试
修正这个错误吧,我看,我们还是别修改MFC了,毕竟MFC好庞大,牵一动全身啊,
修改自己的代码吧,
前面不是说过DLL的InitInstance么?在其中AfxEnableControlContainer前面加上
一条调用:
OleInitialize(NULL)
BOOL CTestApp::InitInstance()
{
::OleInitialize(NULL);
::AfxEnableControlContainer();
return CWinApp::InitInstance();
}
再次运行,OK!!!!!!!!通过了!
至此,调试结束,问题解决。
3,结论
在动态库dll中生成对话框,如果其中包括了ActiveX控件,在创建对话框失败时,
需要考虑在CAPP::InitInstance()中加上如下的语句:
OleInitialize(NULL) & AfxEnableControlContainer()
说起来,很简单哦。
4,解释
下面倒过来解释为什么我们在测试器Tester中插入了WebBrowser2后,Dll不加入
OleInitialize也可以创建带有ActiveX的控件。这个解释好像
就比较简单了,因为测试器在创建WebBrowser2的时候,就调用了OleInitialize来
初始化COM环境(测试器是一个exe,而不是dll,所以前面代
码中AfxOleInit中的所谓afxContextIsDLL就是FALSE了,从而它将调用
OleInitialize),而COM的初始化是针对线程的(而不是模块),恰好
,我们的Dll中的对话框和测试器的对话框都运行在一个线程中,那就是界面线程
,所以就能够创建带有ActiveX的对话框!
5,引申
其实,这个调试过程的确很复杂,然而,如果注意到另外的一些事实,也许不用这
么麻烦,就很容易找到问题所在了,这里要说的是VC在调试
的时候的Output窗口中的Trace信息,我们好像很少关心这些信息(至少我很少关
心MFC的这些信息),这次的调试,其实在Output窗口里面已
经显示了错误的信息,让我们借鉴,复制过来供参考:
...
CoCreateInstance of OLE control {8856F961-340A-11D0-A96B-00C04FD705A2}
failed.
>>> Result code: 0x800401f0
>>> Is the control is properly registered?
Warning: CreateDlgControls failed during dialog init.
...
这里就显示了错误码,0x800401f0,就是COM没有初始化,MFC给出的错误提示包括
:控件创建失败,失败的错误码,翻译过来,发现它在猜测
控件没有注册,却没有怀疑自己的AfxOleInit这个函数没有工作。这却让我对应起
来我们现实生活中的许多人:
问题出来的时候,总怀疑别人的工作出问题了,极少怀疑自己的工作中存在缺陷!
!!
--
一局输赢料不真 香销茶尽尚逡巡
欲知目下兴衰兆 须问旁观冷眼人
※ 来源:·饮水思源 bbs.sjtu.edu.cn·[FROM: 211.80.34.208]
[回复本文][原帖] 发信人: microsoft(小猪战士:重新开始), 信区: VC
标 题: Re: MFC的一个bug,没耐心看,就看看结论好了
发信站: 饮水思源 (2003年10月26日15:28:59 星期天), 站内信件
//
// Note!
//
// If this DLL is dynamically linked against the MFC
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// DLLs, any functions exported from this DLL which
// call into MFC must have the AFX_MANAGE_STATE macro
// added at the very beginning of the function.
//
// For example:
//
// extern "C" BOOL PASCAL EXPORT ExportedFunction()
// {
// AFX_MANAGE_STATE(AfxGetStaticModuleState());
// // normal function body here
// }
//
// It is very important that this macro appear in each
// function, prior to any calls into MFC. This means that
// it must appear as the first statement within the
// function, even before any object variable declarations
// as their constructors may generate calls into the MFC
// DLL.
//
// Please see MFC Technical Notes 33 and 58 for additional
// details.
//
【 在 Captionst (一叶知秋) 的大作中提到: 】
: 在动态链接库中使用对话框,是否应该先
: AFX_MANGAGE_STATE(AfxGetModuleState());
--
一局输赢料不真 香销茶尽尚逡巡
欲知目下兴衰兆 须问旁观冷眼人
※ 来源:·饮水思源 bbs.sjtu.edu.cn·[FROM: 211.80.34.208]
|
[回复本文][原帖] 发信人: Captionst (一叶知秋), 信区: VC
标 题: Re: MFC的一个bug,没耐心看,就看看结论好了
发信站: 饮水思源 (2003年10月26日22:24:02 星期天), 站内信件
Yes.
though you create the DLL with regular options,
yet you use an MFC Dialog in your code, I think
it may be neccessary to add the AFX_MANAGE_STATE macro.
【 在 microsoft (小猪战士:重新开始) 的大作中提到: 】
: //
: // Note!
: //
: // If this DLL is dynamically linked against the MFC
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
: // DLLs, any functions exported from this DLL which
: // call into MFC must have the AFX_MANAGE_STATE macro
: // added at the very beginning of the function.
: //
: // For example:
: .................(以下省略)
|