我们在设计平台类应用程序时,常常会受到一些特殊UI的影响,例如,在一个算法平台中,要加入一个特殊算法的配置对话框,这时,平台维护工程师,肯定不想把这个对话框放在平台中,一是当前的对话框压根和当前模块的业务没有任何关系,另外,平台工程师也不想陷入无尽的UI编写中。
这时,我们可以把这些特殊的UI放在不同的模块中,平台只要调用就可以了。
想法固然是好,但在实践中因为对线程状态,模块状态,线程模块状态理解不够深入,常常会令接口使用者在调用时,出现各种调用失败的错误。以下就是一接口设计中常见的错误。
#ifndef _RPRT_HIGHLEVEL_HEADER
#define _RPRT_HIGHLEVEL_HEADER
#ifndef RPRT_HIGHLEVEL_DLLAPI
#define RPRT_HIGHLEVEL_DLLAPI extern "C" __declspec(dllimport)
#endif
RPRT_HIGHLEVEL_DLLAPI int InitPrintModule();
RPRT_HIGHLEVEL_DLLAPI int ReleasePrintModule();
/**获取收集图像对话框
*rc: 给定的ChildDialog的大小和相对于pWnd位置
*pWnd:当前对话框的父窗口
*/
RPRT_HIGHLEVEL_DLLAPI HWND GetChildDialog(CWnd* pParentWnd ,const CRect rc );
/**获取胶片预览对话框
*/
RPRT_HIGHLEVEL_DLLAPI int ShowPreviewDialog();
/**插入一个打印任务
*strFileName为dicom图像的路径,其中gsps路径是本程序默认组合
*/
RPRT_HIGHLEVEL_DLLAPI int PushOneTask(const CString& strFileName);
#endif
其中,我们着重看GetChildDialog导出函数。客户端代码是
CRect rcReigion;
m_Static_Drag.GetClientRect(rcReigion);
m_Static_Drag.MapWindowPoints(this,&rcReigion);
GetChildDialog(this,rcReigion);
动态库中的代码是
在动态库中,GetChildDialog的实现代码如下
CDlgTest* gpDlgPrintCollect = NULL;
RPRT_HIGHLEVEL_DLLAPI HWND GetChildDialog(CWnd* pParentWnd, CRect rc)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
if (gpDlgPrintCollect == NULL)
{
if (pParentWnd == NULL)
{
return NULL;
}
gpDlgPrintCollect = new CDlgTest(pParentWnd,rc);
gpDlgPrintCollect->ShowWindow(SW_SHOW);
}
return gpDlgPrintCollect->GetSafeHwnd();
}
但,程序在Debug状态下,会引发如下断言
// should also be in the permanent or temporary handle map
CHandleMap* pMap = afxMapHWND();
ASSERT(pMap != NULL);令模块的编写人员,很是郁闷。
自己看注释,实际上很清楚,英文的意思是当前传入的pWnd指针,不在当前线程的临时或者持久的队列中。这个队列又是什么意思呢?
还得从线程安全说起。因为MFC对象不是线程安全(线程安全有两种理解方式:一种是不能同时被两个线程来调用;另外一种是一个线程不能调用另外线程创建的对象。进一步可证实,第二种情况,也是为了防止第一种情况的出现,即,两个线程不能同时操作一个对象)的。
为什么MFC的设计者不把这些对象设计成线程安全的?按照微软的原话,原因有二,一是考录到MFC对象的大小(object size),另外,由于执行效率的考虑(performance)。
微软为了保证MFC对象只有一个线程来调用(为保证对象线程安全,通常的做法是设置访问对象的开关,此开关有用户态的CriticalSection,更有核心态中一些列的Handle:Event,Mutex等;另外的一种方式就是MFC采用的方式,只能由一个线程来调用此对象。MFC中是只能由创建对象的线程来调用此对象),要查当前的MFC对象是否是由当前线程所创建。为了实现此目的,微软的做法是在线程的本地存储(TLS)中,保存线程本地对象表单,这些表单有如下:
HWND (CWnd and CWnd-derived classes)
HDC (CDC and CDC-derived classes)
HMENU (CMenu)
HPEN (CGdiObject)
HBRUSH (CGdiObject)
HFONT (CGdiObject)
HBITMAP (CGdiObject)
HPALETTE (CGdiObject)
HRGN (CGdiObject)
HIMAGELIST (CImageList)
SOCKET (CSocket)
MFC使用AFX_MODULE_THREAD_STATE对象,把上述的对照表单都包装了起来。
这时,可能你要说,当前的调用pParentWnd和gpDlgPrintCollect是同一个线程进行创建的啊。可你要在看AFX_MODULE_THREAD_STATE,从名字上就能猜出,这个量是和线程有关系,同时和当前模块也是有关系的。
你可以google这边文章<<How to Provide Your Own DllMain in an MFC Regular DLL>>,看文章的最后段落,MFC CWnd objects, CDC objects, CMenu objects, GDI objects, and CImageList objects are restricted to a per-thread, per-module basis. In other words, MFC objects created in one module or thread cannot be passed to and/or used in a different module or thread.也就是说不要在MFC的规则动态库中传递诸如,CWnd CDC CMenu GDI CImageList对象。
所以,解决办法应当修改接口为:自己看注释,实际上很清楚,英文的意思是当前传入的pWnd指针,不在当前线程的临时或者持久的队列中。这个队列又是什么意思呢?
还得从线程安全说起。因为MFC对象不是线程安全(线程安全有两种理解方式:一种是不能同时被两个线程来调用;另外一种是一个线程不能调用另外线程创建的对象。进一步可证实,第二种情况,也是为了防止第一种情况的出现,即,两个线程不能同时操作一个对象)的。
为什么MFC的设计者不把这些对象设计成线程安全的?按照微软的原话,原因有二,一是考录到MFC对象的大小(object size),另外,由于执行效率的考虑(performance)。
微软为了保证MFC对象只有一个线程来调用(为保证对象线程安全,通常的做法是设置访问对象的开关,此开关有用户态的CriticalSection,更有核心态中一些列的Handle:Event,Mutex等;另外的一种方式就是MFC采用的方式,只能由一个线程来调用此对象。MFC中是只能由创建对象的线程来调用此对象),要查当前的MFC对象是否是由当前线程所创建。为了实现此目的,微软的做法是在线程的本地存储(TLS)中,保存线程本地对象表单,这些表单有如下:
HWND (CWnd and CWnd-derived classes)
HDC (CDC and CDC-derived classes)
HMENU (CMenu)
HPEN (CGdiObject)
HBRUSH (CGdiObject)
HFONT (CGdiObject)
HBITMAP (CGdiObject)
HPALETTE (CGdiObject)
HRGN (CGdiObject)
HIMAGELIST (CImageList)
SOCKET (CSocket)
MFC使用AFX_MODULE_THREAD_STATE对象,把上述的对照表单都包装了起来。
这时,可能你要说,当前的调用pParentWnd和gpDlgPrintCollect是同一个线程进行创建的啊。可你要在看AFX_MODULE_THREAD_STATE,从名字上就能猜出,这个量是和线程有关系,同时和当前模块也是有关系的。
你可以google这边文章<<How to Provide Your Own DllMain in an MFC Regular DLL>>,看文章的最后段落,MFC CWnd objects, CDC objects, CMenu objects, GDI objects, and CImageList objects are restricted to a per-thread, per-module basis. In other words, MFC objects created in one module or thread cannot be passed to and/or used in a different module or thread.也就是说不要在MFC的规则动态库中传递诸如,CWnd CDC CMenu GDI CImageList对象。
所以,解决办法应当修改接口为:
CDlgTest* gpDlgPrintCollect = NULL;
RPRT_HIGHLEVEL_DLLAPI HWND GetCollectionDialog(HWND hWnd,const CRect rc)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
if (gpDlgPrintCollect == NULL)
{
if (hWnd == NULL)
{
return NULL;
}
CWnd* pTemporayWnd = CWnd::FromHandle(hWnd);
if (pTemporayWnd == NULL)
{
return NULL;
}
gpDlgPrintCollect = new CDlgTest(pTemporayWnd,rc);
gpDlgPrintCollect->ShowWindow(SW_SHOW);
}
return gpDlgPrintCollect->GetSafeHwnd();
}
但是,这个接口已经定义好了,改动起来涉众较广,如果你是Dll的编写者,你可以修改为
CDlgTest* gpDlgPrintCollect = NULL;
RPRT_HIGHLEVEL_DLLAPI HWND GetChildDialog(CWnd* pParentWnd, CRect rc)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
if (gpDlgPrintCollect == NULL)
{
if (pParentWnd == NULL)
{
return NULL;
}
CWnd* pTempWnd = CWnd::FromHandle(pParentWnd->GetSafeHwnd());
gpDlgPrintCollect = new CDlgTest(pParentWnd,rc);
gpDlgPrintCollect->ShowWindow(SW_SHOW);
}
return gpDlgPrintCollect->GetSafeHwnd();
};
最后,解释一下AFX_MANAGE_STATE
AFX_MANAGE_STATE::AFX_MANAGE_STATE(AFX_MODULE_STATE* pNewState)
{
m_pThreadState = _afxThreadState;//获取当前的线程状态
m_pPrevModuleState = m_pThreadState->m_pModuleState;//保存当前的模块状态,当前的模块状态又从线程状态中获取
m_pThreadState->m_pModuleState = pNewState;//采用当前的新的模块状态
}