MFC中,mfc 自定义控件CWnd派生的类如何在线程中发送消息调用mfc 自定义控件函数?

MFC原理与方法_百度文库
两大类热门资源免费畅读
续费一年阅读会员,立省24元!
文档贡献者贡献于
评价文档:
56页免费55页1下载券55页1下载券56页免费41页1下载券 43页免费56页1下载券56页4下载券63页1下载券
喜欢此文档的还喜欢101页免费76页1下载券45页免费442页1下载券69页1下载券
MFC原理与方法|
把文档贴到Blog、BBS或个人站等:
普通尺寸(450*500pix)
较大尺寸(630*500pix)
大小:6.29MB
登录百度文库,专享文档复制特权,财富值每天免费拿!
你可能喜欢首先能够响应消息的类必须都从CCmdTarget类中派生,因为只有以这个类中提供了消息的框架和处理机制,而CWnd类也派生与此类。CWinApp类、CDocument类、CDocTemplate类等都是CCmdTarget的派生类,即子类;而CFrameWnd类、CView类、CDialog类等都是从CWnd中派生的,其实也是CCmdTarget的子孙,所以都能够响应消息,但是响应消息的种类不太相同。
那么,如果自己定义的类要求响应命令消息(就是WM_COMMAND,也就是一些菜单、工具栏中的消息,包括快捷键,这类消息处理的机制与其他以WM_开头的消息处理机制不同,它具有一条层次明确的消息流动路径),那么自定义的类可以从CCmdTarget中派生。由于CWnd窗体类派生于CCmdTarget父类,那么从CWnd中派生的类也可以理所应当的响应命令消息。这种命令消息无论是往已有的一些诸如CWinApp类中还是自定义的类中添加都是一件非常容易的事情,只需用向导即可,在此不再叙述。
如果用户自定义的类要求响应普通的Windows消息(也就是以WM_开头,除了WM_COMMAND以外的消息,这类消息在WM_USER以下的是系统消息,WM_USER以上的可以由用户自己定义),那就要求自定义的类必须从CWnd中派生。这是由于此类消息的处理机制决定的,这类消息没有命令消息那条繁琐的流动路径,而是消息发出者直接发给对应CWnd的窗体句柄,由CWnd负责消息的响应。所以这类消息必须同一个CWnd类对应,更精确的说必须与一个HWND类型的窗体句柄相对应。这样得出一个重要的结论,就是从CCmdTarget中派生而没有从CWnd派生的类没有处理此类消息的能力。
综上所述,就是为什么命令消息可以放到大部分类中处理,包括CWinThread、CWinApp、CDocument、CView、CFrameWnd或是自定义的类中,而普通Windows消息和用户自定义的消息只能放到CFrameWnd和CView等派生与CWnd中的类中处理。
由此可见,我们自定义的类要想响应自定义消息就只能从CWnd中派生(当然不响应任何消息的类可以从CObject中派生)。先来看看如何自定义消息:
在.h中做的工作:
第一步要声明消息:
#define WM_MYMSG WM_USER+8
第二步要在类声明中声明消息映射:
DECLARE_MESSAGE_MAP()
第三步要在类声明中定义消息处理函数:
afx_msg LRESULT MyMsgHandler(WPARAM,LPARAM);
在.cpp中做的工作:
第四步要实现消息映射:
BEGIN_MESSAGE_MAP(CMainFrame, CMDIFrameWnd)ON_MESSAGE(WM_MYMSG,OnMyMsgHandler)END_MESSAGE_MAP()
第五步要实现消息处理函数(当然可以不实现):
LRESULT CMainFrame::OnMyMsgHandler(WPARAM w,LPARAM l){AfxMessageBox("Hello,World!");return 0; }
在引发或发出消息的地方只用写上:
::SendMessge(::AfxGetMainWnd()-&m_hWnd,WM_MYMSG,0,0);
到此,自定义消息完毕,这是好多网上文章都写的东西。大家会发现上面代码是在CMainFrame类中实现的,但是如果要用自定义类,就没有那么简单了。显然把第四步与第五步的CMainFrame换成自定义的类名(这里我用CMyTestObject来代表自定义类)是不能正常工作的。原因在于在发送消息的SendMessage函数中的第一个参数是要响应消息对应的HWND类型的窗体句柄,而CMyTestObject类中的m_hWnd中在没有调用CWnd::Create之前是没有任何意义的,也就是没有调用CWnd::Create或CWnd::CreateEx函数时,CWnd不对应任何窗体,消息处理不能正常运作。
所以,又一个重要的结论,在自定义类能够处理任何消息之前一定要确保m_hWnd关联到一个窗体,即便这个窗体是不可见的。那么有人说,在自定义类的构造函数中调用Create函数就行了,不错,当然也可以在别处调用,只要确保在消息发送之前。但是,Create的调用很有说法,要注意两个地方,第一个参数是类的名称,我建议最好设为NULL;第五个参数是父窗体对象的指针,这个函数指定的对象一定要存在,我建议最好为整个程序的主窗体。还有很多人问第六个参数的意义,这个参数关系不大,是子窗体ID,用于传给父窗体记录以便识别。如下是我的自定义类的构造函数:
CMyTestObject::CMyTestObject(){CWnd::Create(NULL,"MyTestObject",WS_CHILD,CRect(0,0,0,0),::AfxGetMainWnd(),1234);}&&& //一定要在生成主窗体后使用,在主窗体完成OnCreate消息的处理后
CMyTestObject::CMyTestObject(CWnd *pParent){CWnd::Create(NULL,"MyTestObject",WS_CHILD,CRect(0,0,0,0),pParent,1234);}
不能如下调用Create,因为此时CMyTestObject不关联任何窗体,所以this中的m_hWnd无效:
CWnd::Create(NULL,"MyTestObject",WS_CHILD,CRect(0,0,0,0),this,1234);
这时上面四、五两步修改成:
BEGIN_MESSAGE_MAP(CMyTestObject, CWnd)ON_MESSAGE(WM_MYMSG,OnMyMsgHandler)END_MESSAGE_MAP()
LRESULT CMyTestObject::OnMyMsgHandler(WPARAM w,LPARAM l){AfxMessageBox("My Messge Handler in My Self-Custom Class!");return 0; }
在类外部发出消息:
CMyTestObject *test=new CMyTestObject();::SendMessage(test-&m_hWnd,WM_MYMSG,0,0);
在类内部某个成员函数(方法)中发出消息:
::SendMessage(m_hWnd,WM_MYMSG,0,0);
最后一个问题便是容易产生警告错误的窗体回收,自定义的类要显式调用窗体销毁,析构函数如下:
CMyTestObject::~CMyTestObject(){CWnd::DestroyWindow();}
本文来自CSDN博客,转载请标明出处:
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:333011次
积分:4920
积分:4920
排名:第1818名
原创:116篇
转载:243篇
评论:93条
这里是老王的自留地,将自己在工作学习中的所想所学做一下记录和总结。同时希望与同道中人切磋共同进步。如果文章有错误和不足请大家一定不吝指正。
(2)(7)(2)(5)(4)(2)(1)(3)(1)(1)(1)(2)(8)(7)(1)(2)(9)(9)(38)(7)(4)(3)(4)(17)(17)(1)(15)(4)(10)(9)(5)(5)(30)(11)(35)(21)(10)(22)(7)(8)(8)(1)您现在正在浏览:
MFC界面包装类(多线程时成员函数调用的断言失败)
发布时间:
来源: 网络
摘要: 让包装类对象随线程的不同而不同可以对包装类对象进行线程保护,也就是说一个线程不可以也不应该访问另一...
MFC界面包装类(多线程时成员函数调用的断言失败)
MFC界面包装类
——多线程时成员函数调用的断言失败
经常在论坛上看到如下的问题:
DWORD WINAPI ThreadProc( void *pData )
// 线程函数(比如用于从COM口获取数据)
{
// 数据获取循环
// 数据获得后放在变量i中
CAbcDialog *pDialog = reinterpret_cast( pData );
ASSERT( pDialog );
// 此处如果ASSERT_VALID( pDialog )将断言失败
pDialog->m_Data =
pDialog->UpdateData( FALSE );
// UpdateData内部ASSERT_VALID( this )断言失败

}
BOOL CAbcDialog::OnInitDialog()
{
CDialog::OnInitDialog();
// 其他初始化代码
CreateThread( NULL, 0, ThreadProc, this, 0, NULL );
// 创建线程
return TRUE;
}
注意上面注释中的两处断言失败,本文从MFC底层的实现来解释为什么会断言失败,并说明MFC为什么要这样实现及相应的处理办法。
在说明MFC界面包装类的底层实现之前,由于其和窗口有关,故先讲解窗口类这个基础知识以为后面做铺垫。
窗口类
窗口类是一个结构,其一个实例代表着一个窗口类型,与C++中的类的概念非常相近(虽然其表现形式完全不同,C++的类只不过是内存布局和其上的操作这个概念的类型),故被称作为窗口类。
窗口是具有设备操作能力的逻辑概念,即一种能操作设备(通常是显示器)的东西。由于窗口是窗口类的实例,就象C++中的一个类的实例,是可以具有成员函数的(虽然表现形式不同),但一定要明确窗口的目的——操作设备(这点也可以从Microsoft针对窗口所制订的API的功能看出,主要出于对设备操作的方便)。因此不应因为其具有成员函数的功能而将窗口用于功能对象的创建,这虽然不错,但是严重违反了语义的需要(关于语义,可参考我的另一篇文章——《语义的需要》),是不提倡的,但却由于MFC界面包装类的加入导致大多数程序员经常将逻辑混入界面。
窗口类是个结构,其中的大部分成员都没什么重要意义,只是Microsoft一相情愿制订的,如果不想使用界面API(Windows User Interface API),可以不管那些成员。其中只有一个成员是重要的——lpfnWndProc,消息处理函数。
外界(使用窗口的代码)只能通过消息操作窗口,这就如同C++中编写的具有良好的面向对象风格的类的实例只能通过其公共成员函数对其进行操作。因此消息处理函数就代表了一个窗口的一切(忽略窗口类中其他成员的作用)。很容易发现,窗口这个实例只具有成员函数(消息处理函数),不具有成员变量,即没有一块特定内存和一特定的窗口相关联,则窗口将不能具有状态(Windows还是提供了Window Properties API来缓和这种状况)。这也正是上面问题发生的根源。
为了处理窗口不能具有状态的问题(这其实正是Windows灵活的表现),可以有很多种方法,而MFC出于能够很容易的对已有窗口类进行扩展,选择了使用一个映射将一个窗口句柄(窗口的唯一标示符)和一个内存块进行绑定,而这块内存块就是我们熟知的MFC界面包装类(从CWnd开始派生延续)的实例。
MFC状态
状态就是实例通过某种手段使得信息可以跨时间段重现,C++的类的实例就是由外界通过公共成员函数改变实例的成员变量的值以实现具有状态的效果。在MFC中,具有三种状态:模块状态、进程状态、线程状态。分别为模块、进程和线程这三种实例的状态。由于代码是由线程运行,且和另外两个的关系也很密切,因此也被称作本地数据。
模块本地数据
具有模块本地性的变量。模块指一个加载到进程虚拟内存空间中的PE文件,即exe文件本身和其加载的dll文件。而模块本地性即同样的指针,根据代码从不同的模块执行而访问不同的内存空间。这其实只用每个模块都声明一个全局变量,而前面的“代码”就在MFC库文件中,然后通过一个切换的过程(将欲使用的模块的那个全局变量的地址赋给前述的指针)即可实现模块本地性。MFC中,这个过程是通过调用AfxSetModuleState来切换的,而通常都使用AFX_MANAGE_STATE这个宏来处理,因此下面常见的语句就是用于模块状态的切换的:
AFX_MANAGE_STATE( AfxGetStaticModuleState() );
MFC中定义了一个结构(AFX_MODULE_STATE),其实例具有模块本地性,记录了此模块的全局应用程序对象指针、资源句柄等模块级的全局变量。其中有一个成员变量是线程本地数据,类型为AFX_MODULE_THREAD_STATE,其就是本文问题的关键。
进程本地数据
具有进程本地性的变量。与模块本地性相同,即同一个指针,在不同进程中指向不同的内存空间。这一点Windows本身的虚拟内存空间这个机制已经实现了,不过在dll中定义的全局变量,如果dll支持Win32s,则其是共享其全局变量的,即不同的进程加载了同一dll将访问同一内存。Win32s是为了那些基于Win32的应用程序能在Windows 3.1上运行,由于Windows 3.1是16位操作系统,早已被淘汰,而现行的dll模型其本身就已经实现了进程本地性(不过还是可以通过共享节来实现Win32s中的dll的效果),因此进程状态其实就是一全局变量。
MFC中作为本地数据的结构有很多,如_AFX_WIN_STATE、_AFX_DEBUG_STATE、_AFX_DB_STATE等,都是MFC内部自己使用的具有进程本地性的全局变量。
线程本地数据
具有线程本地性的变量。如上,即同一个指针,不同的线程将会访问不同的内存空间。这点MFC是通过线程本地存储(TLS——Thread Local Storage,其使用方法由于与本文无关,在此不表)实现的。
MFC中定义了一个结构(_AFX_THREAD_STATE)以记录某些线程级的全局变量,如最近一次的模块状态指针,最近一次的消息等。
模块线程状态
MFC中定义的一个结构(AFX_MODULE_THREAD_STATE),其实例即具有线程本地性又具有模块本地性。也就是说不同的线程从同一模块中和同一线程从不同模块中访问MFC库函数都将导致操作不同的内存空间。其应用在AFX_MODULE_STATE中,记录一些线程相关但又模块级的数据,如本文的重点——窗口句柄映射。
包装类对象和句柄映射
句柄映射——CHandleMap,MFC提供的一个底层辅助类,程序员是不应该直接使用它的。其有两个重要的成员变量:CMapPtrToPtr m_permanentMap, m_temporaryM。分别记录永久句柄绑定和临时句柄绑定。前面说过,MFC使用一个映射将窗口句柄和其包装类的实例绑定在一起,m_permanentMap和m_temporaryMap就是这个映射,分别映射永久包装类对象和临时包装类对象,而在前面提到过的AFX_MODULE_THREAD_STATE中就有一个成员变量:CHandleMap* m_pmapHWND;(之所以是CHandleMap*是使用懒惰编程法,尽量节约资源)以专门完成HWND的绑定映射,除此以外还有如m_pmapHDC、m_pmapHMENU等成员变量以分别实现HDC、HMENU的绑顶映射。而为什么这些映射要放在模块线程状态而不放在线程状态或模块状态是很明显的——这些包装类包装的句柄都是和线程相关的(如HWND只有创建它的线程才能接收其消息)且这个模块中的包装类对象可能不同于另一个模块的(如包装类是某个DLL中专门派生的一个类,如a.dll中定义的CAButton的实例和b.dll中定义的CBButton的实例如果同时在一个线程中。此时线程卸载了a.dll,然后CAButton的实例得到消息并进行处理,将发生严重错误——类代码已经被卸载掉了)。
包装类存在的意义有二:包装对HWND的操作以加速代码的编写和提供窗口子类化(不是超类化)的效果以派生窗口类。包装类对象针对线程分为两种:永久包装类对象(以后简称永久对象)和临时包装类对象(以后简称临时对象)。临时对象的意义仅仅只有包装对HWND的操作以加速代码编写,不具有派生窗口类的功能。永久对象则具有前面说的包装类的两个意义。
在创建窗口时(即CWnd::CreateEx中),MFC通过钩子提前(WM_CREATE和WM_NCCREATE之前)处理了通知,用AfxWndProc子类化了创建的窗口并将对应的CWnd*加入当前线程的永久对象的映射中,而在AfxWndProc中,总是由CWnd::FromHandlePermanent(获得对应HWND的永久对象)得到当前线程中当前消息所属窗口句柄对应的永久对象,然后通过调用得到的CWnd*的WindowProc成员函数来处理消息以实现派生窗口类的效果。这也就是说永久对象具有窗口子类化的意义,而不仅仅是封装HWND的操作。
要将一个HWND和一个已有的包装类对象相关联,调用CWnd::Attach将此包装类对象和HWND映射成永久对象(但这种方法得到的永久对象不一定具有子类化功能,很可能仍和临时对象一样,仅仅起封装的目的)。如果想得到临时对象,则通过CWnd::FromHandle这个静态成员函数以获得。临时对象之所以叫临时,就是其是由MFC内部(CHandleMap::FromHandle)生成,其内部(CHandleMap::DeleteTemp)销毁(一般通过CWinThread::OnIdle中调用AfxUnlockTempMaps)。因此程序员是永远不应该试图销毁临时对象的(即使临时对象所属线程没有消息循环,不能调用Cw
MFC界面包装类(多线程时成员函数调用的断言失败)第2部分:inThread::OnIdle,在线程结束时,CHandleMap的析构仍然会销毁临时对象)。
原因
为什么要分两种包装类对象?很好玩吗?注意前面提过的窗口模型——只能通过消息机制和窗口交互。注意,也就是说窗口是线程安全的实例。窗口过程的编写中不用考虑会有多个线程同时访问窗口的状态。如果不使用两种包装类对象,在窗口创建的钩子中通过调用SetProp将创建的窗口句柄和对应的CWnd*绑定,不一样也可以实现前面说的窗口句柄和内存块的绑定?
CWnd的派生类CA,具有一个成员变量m_BGColor以决定使用什么颜色填充底背景。线程1创建了CA的一个实例a,将其指针传进线程2,线程2设置a.m_BGColor为红色。这已经很明显了,CA::m_BGColor不是线程安全的,如果不止一个线程2,那么a.m_BGColor将会出现线程访问冲突。这严重违背窗口是线程安全的这个要求。因为使用了非消息机制与窗口进行交互,所以失败。
继续,如果给CA一个公共成员函数SetBGColor,并在其中使用原子操作以保护m_BGColor,不就一切正常了?呵,在CA::OnPaint中,会两次使用m_BGColor进行绘图,如果在两次绘图之间另一线程调用CA::SetBGColor改变了CA::m_BGColor,问题严重了。也就是说不光是CA::m_BGColor的写操作需要保护,读操作亦需要保护,而这仅仅是一个成员变量。
那么再继续,完全按照窗口本身的定义,只使用消息与它交互,也就是说自定义一个消息,如AM_SETBGCOLOR,然后在CA::SetBGColor中SendMessage这个消息,并在其响应函数中修改CA::m_BGColor。完美了,这是即符合窗口概念又很好的设计,不过它要求每一个程序员编写每一个包装类时都必须注意到这点,并且最重要的是,C++类的概念在这个设计中根本没有发挥作用,严重地资源浪费。
因此,MFC决定要发挥C++类的概念的优势,让包装类对象看起来就等同于窗口本身,因此使用了上面的两种包装类对象。让包装类对象随线程的不同而不同可以对包装类对象进行线程保护,也就是说一个线程不可以也不应该访问另一个线程中的包装类对象(因为包装类对象就相当于窗口,这是MFC的目标,并不是包装类本身不能被跨线程访问),“不可以”就是通过在包装类成员函数中的断言宏实现的(在CWnd::AssertValid中),而“不应该”前面已经解释地很清楚了。因此本文开头的断言失败的根本原因就是因为违反了“不可以”和“不应该”。
虽然包装类对象不能跨线程访问,但是窗口句柄却可以跨线程访问。因为包装类对象不仅等同于窗口,还改变了窗口的交互方式(这也正是C++类的概念的应用),使得不用非得使用消息机制才能和窗口交互。注意前面提到的,如果跨线程访问包装类对象,而又使用C++类的概念操作它,则其必须进行线程保护,而“不能跨线程访问”就消除了这个问题。因此临时对象的产生就只是如前面所说,方便代码的编写而已,不提供子类化的效果,因为窗口句柄可以跨线程访问。
解决办法
已经了解失败的原因,因此做如下修改:
DWORD WINAPI ThreadProc( void *pData )
// 线程函数(比如用于从COM口获取数据)
{
// 数据获取循环
// 数据获得后放在变量i中
CAbcDialog *pDialog = static_cast(
CWnd::FromHandle( reinterpret_cast( pData ) ) );
ASSERT_VALID( pDialog );
// 此处可能断言失败
pDialog->m_Data =
// 这是不好的设计,详情可参看我的另一篇文章:《语义的需要》
pDialog->UpdateData( FALSE );
// UpdateData内部ASSERT_VALID( this )可能断言失败

}
BOOL CAbcDialog::OnInitDialog()
{
CDialog::OnInitDialog();
// 其他初始化代码
CreateThread( NULL, 0, ThreadProc, m_hWnd, 0, NULL );
// 创建线程
return TRUE;
}
之所以是“可能”,因为这里有个重点就是临时对象是HWND操作的封装,不是窗口类的封装。因此所有的HWND临时对象都是CWnd的实例,即使上面强行转换为CAbcDialog*也依旧是CWnd*,所以在ASSERT_VALID里调用CAbcDialog::AssertValid时,其定义了一些附加检查,则可能发现这是一个CWnd的实例而非一个CAbcDialog实例,导致断言失败。因此应将CAbcDialog全部换成CWnd,这下虽然不断言失败了,但依旧错误(先不提pDialog->m_Data怎么办),因为临时对象是HWND操作的封装,而不幸的是UpdateData只是MFC自己提供的一个对话框数据交换的机制(DDX)的操作,其不是通过向HWND发送消息来实现的,而是通过虚函数机制。因此在UpdateData中调用实例的DoDataExchange将不能调用CAbcDialog::DoDataExchange,而是调用CWnd::DoDataExchange,因此将不发生任何事。
因此合理(并不一定最好)的解决方法是向CAbcDialog的实例发送一个消息,而通过一个中间变量(如一全局变量)来传递数据,而不是使用CAbcDialog::m_Data。当然,如果数据少,比如本例,就应该将数据作为消息参数进行传递,减少代码的复杂性;数据多则应该通过全局变量传递,减少了缓冲的管理费用。修改后如下:
#define AM_DATANOTIFY ( WM_USER + 1 )
static DWORD g_Data = 0;
DWORD WINAPI ThreadProc( void *pData )
// 线程函数(比如用于从COM口获取数据)
{
// 数据获取循环
// 数据获得后放在变量i中
g_Data =
CWnd *pWnd = CWnd::FromHandle( reinterpret_cast( pData ) );
ASSERT_VALID( pWnd );
// 本例应该直接调用平台SendMessage而不调用包装类的,这里只是演示
pWnd->SendMessage( AM_DATANOTIFY, 0, 0 );

}
BEGIN_MESSAGE_MAP( CAbcDialog, CDialog )

ON_MESSAGE( AM_DATANOTIFY, OnDataNotify )

END_MESSAGE_MAP()
BOOL CAbcDialog::OnInitDialog()
{
CDialog::OnInitDialog();
// 其他初始化代码
CreateThread( NULL, 0, ThreadProc, m_hWnd, 0, NULL );
// 创建线程
return TRUE;
}
LRESULT CAbcDialog::OnDataNotify( WPARAM /* wParam */, LPARAM /* lParam */ )
{
UpdateData( FALSE );
return 0;
}
void CAbcDialog::DoDataExchange( CDataExchange *pDX )
{
CDialog::DoDataExchange( pDX );
DDX_Text( pDX, IDC_EDIT1, g_Data );
}中定义了一个结构(AFX_MODULE_STATE),其实例具有模块本地性,记录了此模块的全局应用程序对象指针、资源句柄等模块级的全局变量。其中有一个成员变量是线程本地数据,类型为
上一篇:没有了
本周热门资讯排行

我要回帖

更多关于 mfc 自定义控件 的文章

 

随机推荐