回调函数、线程和 MFC






4.94/5 (19投票s)
2000年5月17日

188921
了解如何使用 MFC 的回调函数和线程。
许多程序员似乎都会遇到一个棘手的问题,那就是使用回调函数。这些回调函数是 Windows API 工作方式的一部分,有些是 枚举器 的结果,它们提供对底层 Windows 信息集的访问。
本文讨论了如何从原始 API 回调函数域迁移到 MFC 域,这大大简化了您实际编写处理回调函数的代码的方式。本文将主要在回调函数的上下文中进行讨论,并以一个关于如何在 MFC 类中创建工作线程的示例结束。
回调函数,特别是枚举器
有很多有用的方法可用于查找信息;例如 EnumFontFamiliesEx
、EnumWindows
等等。对于其中一些,没有其他有效的方法来枚举对象。对于其他一些,例如 EnumWindows
,该方法是强烈推荐的枚举方法(例如,EnumWindows
即使在其他线程可能在您枚举它们的同时创建或删除窗口时也能工作)。
许多人看到的真正问题是,这些是原始 API 函数,在 MFC 中是 ::EnumWindows
、::EnumChildWindows
、::EnumFontFamiliesEx
等等。您得到的是一个原始 API 对象,例如 HWND
,生活变得困难,因为您无法访问进行枚举的实例或其任何 MFC 属性。例如,您可能想将字体列表放入列表框,或将窗口列表放入组合框,或类似的操作。
这通常迫使不必要地使用全局变量,并过分依赖 GetDlgItem
,这两者都是根本上的错误(如果您想知道 GetDlgItem
的错误之处,请参阅我关于此主题的 文章)。
对于大多数枚举器,可以通过使用调用中的 LPARAM
参数完全避免这种情况。例如,几个枚举函数的设计是
int EnumFontFamiliesEx(HDC, LPLOGFONT, FONTENUMPROC, LPARAM, DWORD); BOOL EnumChildWindows(HWND, WNDENUMPROC, LPARAM); BOOL EnumWindows(WNDENUMPROC, LPARAM); BOOL EnumThreadWindows(DWORD, WNDENUMPROC, LPARAM); BOOL EnumMetaFile(HDC, HMETAFILE, MFENUMPROC, LPARAM); BOOL EnumResourceNames(HMODULE, LPCTSTR, ENUMRESOURCENAMESPROC, LONG /* LPARAM */); BOOL EnumResoureTypes(HMODULE, ENUMRESOURCETYPESPROC, LONG /* LPARAM */);
遗憾的是,由于我认为是一个严重的设计缺陷,有些枚举器*不*接受 LPARAM
,例如
EnumDateFormats EnumDateFormatsEx EnumTimeFormats
这使得生活变得困难;这里不可原谅的是,在使用 LPARAM
的方法在这些函数设计时就已经广为人知。
以上仅为代表性示例。此外,还有许多“Enum...
”函数不使用回调函数,这些函数在本讨论中可以安全地忽略。
为了举例,我们将看一个典型的回调枚举函数 EnumWindows
。请注意,我即将描述的回调技术适用于*所有*回调函数,无论是来自枚举器还是其他来源,您都可以为其提供任意的 LPARAM
风格的值。
使用 EnumWindows
时,请记住您调用的函数无法访问您的类,除非您采取特定措施使其可以访问。例如,
EnumWindows(enumwndfn, NULL)
将调用枚举函数 enumwndfn
,该函数必须是一个普通的 C 函数,或一个静态类成员(因此无法访问类的任何实例,例如您的 CDialog 派生类)
解决此问题的首选方法是使用 LPARAM
值传递类实例
EnumWindows(enumwndfn, (LPARAM)this)
将这两个声明添加到您的类中
static BOOL CALLBACK enumwndfn(HWND hWnd, LPARAM lParam);
BOOL enumwndfn(CWnd * wnd);
我经常为两者使用相同的名称,以简化我需要处理的概念数量;C++ 的重载规则有助于区分它们。请仔细注意“static”一词用于限定第一个声明。其后果是调用中没有隐式的 this
参数,这也意味着该方法没有隐式访问类实例。
在静态成员函数的实现中,请执行以下操作
BOOL CALLBACK CMyDialog::enumwndfn(HWND hWnd, LPARAM lParam)
{
CMyDialog * me = (CMyDialog *)lParam;
return me->enumwndfn(CWnd::FromHandle(hWnd);
}
对于非静态成员,您现在可以直接访问类对象,例如,列表框 c_Windows
BOOL CMyDialog::enumwndfn(CWnd * wnd)
{
// full access to class here
CString s;
wnd->GetWindowText(s);
c_Windows.AddString(s);
}
这对于 MFC 编程显然更方便。
所以,您会说:“但我确实使用 LPARAM
来传递信息!现在我该怎么办?”答案很简单。简单来说:将您过去通过 LPARAM
传递的信息存储在类的实例变量中。与其访问 lParam
变量,不如编写如下代码:
enumParam = whatever; EnumWindows(enumwndfn, this); BOOL CMyDialog::enumwndfn(CWnd * wnd) { if(enumParam == whatever) ... }
现在,有些人会说:“啊哈!抓到你了!如果我有多条线程在进行枚举,这种技术就不是线程安全的!”确实如此。所以有一个稍微复杂一点的解决方案。
typedef struct EnumParm {CMyDialog * me; LPARAM lParam;}
然后编写
EnumParam p;
p.me = this;
p.lParam = whatever;
EnumWindows(enumwndfn, (LPARAM)&p);
并重写处理程序为:
BOOL CMyDialog::enumwndfn(HWND hWnd, LPARAM lParam) { EnumParm * p = (EnumParm *)lParam; return p->me->enumwndfn(CWnd::FromHandle(hWnd), p->lParam); } BOOL CMyDialog::enumwndfn(CWnd * wnd, LPARAM lParam) { // enumeration handler code here }
问题就解决了。
类中的工作线程
我使用相同的技术来使用工作线程。我经常希望工作线程在我的类的上下文中作为一组 MFC 代码运行,尽管它不是一个基于 GUI 的线程。相同的转换技术也适用
static UINT threading(LPVOID p);
void threading();
要启动线程,请调用
AfxBeginThread(threading, this);
代码如下:
UINT CMyClass::threading(LPVOID p) { CMyClass * me = (CMyClass *)p; me->threading(); return 0; } void CMyClass::threading() { // complete class instance access available }
摘要
使用原始 API 进行回调函数不需要您离开 MFC 域并开始“手动编码”所有内容。在域之间切换通常很容易,正如本文所示。
这些文章中表达的观点是作者的观点,不代表,也不被微软认可。