附加和分离对象






4.98/5 (25投票s)
2000年5月17日

134188
将 MFC 对象附加到 Windows 对象以及与 Windows 对象分离。
引言
MFC 提供了一组“包装器对象”,其中包含嵌入的 Windows 对象。例如,CWnd 包装 HWND,CFont 包装 HFONT,CBrush 包装 HBRUSH,依此类推。这些在下表中进行了总结。MFC 和 Windows 对象之间存在一些重要的交互,您需要了解它们。
未能处理这些问题可能导致意外的 ASSERT 语句、访问错误、对象消失以及其他更微妙的问题。
本文旨在阐明处理 MFC/Windows 接口的问题。
MFC 对象 | Windows 对象 | (变体) |
CWnd |
HWND |
任何窗口 |
CEdit |
HWND |
编辑 |
CListBox |
HWND |
列表框 |
CStatic |
HWND |
静态 |
CComboBox |
HWND |
组合框 |
CGDIObject |
(GDI 对象) |
(任意) |
CBitmap |
HBITMAP |
|
CBrush |
HBRUSH |
|
CPen |
HPEN |
|
CFont |
HFONT |
|
CRegion |
HRGN |
|
CPalette |
HPALETTE |
创建:Windows 两步法
大多数对象的创建涉及一个两步过程。第一步是创建一个 C++ 对象,它是 Windows 对象的“包装器”。下一步是创建实际的 Windows 对象。某些带参数的构造函数一次完成这两个步骤。例如,
CPen pen;
创建了一个 MFC 对象 CPen,但没有为其关联 HPEN。但是构造函数
CPen pen(PS_SOLID, 0, RGB(255, 0, 0));
创建一个 MFC 对象 CPen,然后创建底层 Windows 对象 HPEN,并将其附加到 CPen。
您可以通过使用 Create 方法隐式完成此操作(在我看来,该方法有时被随意重命名,因为 MFC 的设计者不是 C++ 专家)。例如,要创建一支画笔,您可以这样做
CPen pen; pen.CreatePen(PS_SOLID, 0, RGB(255, 0, 0));
(MFC 有 CreatePen 和 CreatePenIndirect,这很愚蠢,因为 CPen 或 CGDIObject 超类中都没有 Create 方法)。
包装器类和对象
将 Windows 对象附加到 MFC 对象有一个严重的含义。当 MFC 对象被销毁时,附加的 Windows 对象也会被销毁。这有一些严重的后果。许多程序员会犯的一个常见错误是
{ CFont f; f.CreateFont(...); c_InputData.SetFont(&f); }
他们对控件上似乎没有效果感到惊讶。这很神奇,因为他们以前做过类似的事情
{ CFont f; f.CreateStockObject(ANSI_FIXED_FONT); c_DisplayData.SetFont(&f); }
并且完美地奏效了!
实际上,第二个例子并没有比第一个例子好。它之所以成功,是因为他们不知情地利用了一个特殊情况。
第一个例子中发生的情况是 CFont 对象按预期在堆栈上创建。CreateFont 及其冗长的参数列表随后创建了一个 HFONT 对象,由其句柄值表示,并将 HFONT 附加到 CFont。到目前为止一切都很好。已在窗口引用 c_InputData(一个 CEdit 控件)上调用 SetFont 方法(如果您不知道如何执行此操作,请阅读我关于避免 GetDlgItem 的文章。这最终会向编辑控件发送一条消息,我们可以在下面简化显示(如果您想了解详细信息,可以阅读 MFC 代码)。
void CWnd::SetFont(CFont * font)
{
::SendMessage(m_hWnd, WM_SETFONT, font->m_hObject, TRUE);
}
请注意,发送到控件的是 HFONT 值。到目前为止一切都好。
现在我们离开声明变量的块。调用了析构函数 CFont::~CFont。当调用包装器的析构函数时,将销毁关联的 Windows 对象。析构函数的解释可以简化并用以下代码说明(再次,真相有点复杂,您可以自己阅读 MFC 源代码)
CFont::~CFont()
{
if(m_hObject != NULL)
{
::DeleteObject(m_hObject);
}
}
当编辑控件在其 WM_PAINT 处理程序中绘制自身时,它希望将其关联的字体选择到其设备上下文 (DC) 中。您可以想象代码的形式如下。这是非常简化的代码,旨在指示性而非决定性,您不会在 MFC 源代码中找到它,因为它属于底层 Windows 实现。
edit_OnPaint(HWND hWnd) { HDC dc; PAINTSTRUCT ps; HFONT font; dc = BeginPaint(hWnd, &ps); font = SendMessage(hWnd, WM_GETFONT, 0, 0); SelectObject(dc, font); TextOut(dc, ...); }
现在看看发生了什么。CFont 被销毁,这反过来又销毁了 HFONT。但是 HFONT 已经传递给了 EDIT 控件,并且在那里。当在编辑处理程序中执行 SelectObject 时,它指定了一个无效的句柄,因此 SelectObject 被忽略。因此,似乎没有变化。
那么,为什么在选择 ANSI_FIXED_FONT 时会起作用呢?嗯,*内置对象*具有特殊属性,其中一项特殊属性是 DeleteObject 对内置对象无效。所以实际上代码是错误的,而且之所以起作用,仅仅是因为内置对象从未被实际删除。(如果您听说过不能删除内置对象,那么您要么是 Windows 3.0 (Win16) 程序员,要么与这样做过的人交谈过。此 bug 已在 16 位 Windows 3.1 发布时修复。)
您如何解决这个问题?请继续阅读...
分离包装器和对象:Detach()
程序员经常使用的一种解决方案是
{
CFont * f;
f = new CFont;
f->CreateFont(...);
c_EditControl.SetFont(f);
}
经验观察表明,此代码有效且正确。这是真的。它有效。但这是懒惰的代码。那个 CFont* 引用的 CFont 对象到底发生了什么?什么都没有,这就是发生的。那里有一个未达到,且无法销毁的恶意 CFont。它将永远存在。这*可能*无害,但不是好的编程实践。
我的做法如下,这在使用 MFC 的 MFC-到-Windows 接口时是一个非常重要的技巧。我使用 Detach 方法
{
CFont f;
f.CreateFont(...);
c_InputData.SetFont(&f);
f.Detach(); // VERY IMPORTANT!!!
}
Detach 操作将 Windows 对象与其包装器分离,并返回底层 Windows 对象句柄作为其值。因为我不需要它,所以我没有将其分配给任何东西。但是现在,当 CFont 被销毁时,关联的 m_hObject 句柄为 NULL,并且底层 HFONT 不会被销毁。
如果您阅读我的代码,您会发现很多 Detach 的实例。例如,我的窗口位图捕获函数它获取指定窗口的位图并将其放入剪贴板。为了防止位图在作用域结束时被销毁,它将位图与 CBitmap 对象分离。
当 Detach 不够用时:CDialog 和无模式对话框
在各种论坛中,一个常见的问题是“我尝试创建我的无模式对话框,但失败了。我从未得到对话框。出了什么问题?”并附带一段类似以下的代码片段
void CMyClass::OnLaunchDialog()
{
CMyToolbox dlg;
dlg.Create(dlg.IDD);
}
此时,您应该已经明白了发生了什么。对话框已创建,但一旦包含变量的作用域结束,析构函数就会进来,并且由于这是一个窗口,它会对关联的对象调用 DestroyWindow。当然,此技术对于模态对话框始终有效,如
{ CMyDataEntryScreen dlg; dlg.DoModal(); }
因为模态对话框退出后,dlg 变量就不再需要了。
(我不明白为什么我必须为 Create 方法提供对话框 ID,因为它在类中是隐式的!)
但是您不能在这里使用 Detach,因为对话框需要处理消息并且需要有状态。我从未测试过,但我怀疑如果您确实执行了 Detach,您可能会开始收到大量 ASSERT 失败,或者会遇到某种访问冲突。您确实需要那个无模式对话框对象!
在这种情况下,正确的方法是创建一个 CDialog 引用,例如,在您的 CWinApp 类中添加以下行(这假设您只希望所有窗口实例有一个工具箱)
// in your CWinApp class CMyToolbox * tools; // in your CWinApp constructor: tools = NULL; // in your activator: void CWinApp::OnOpenToolbox() { if(tools != NULL) { /* use existing */ if(tools->IsIconic()) tools->ShowWindow(SW_RESTORE; tools->SetForegroundWindow(); return; } /* use existing */ tools = new CMyToolbox; tools->Create(CMyToolBox::IDD); }
这将创建窗口(如果不存在),如果存在,它将将其带到前面。此代码假定窗口可以最小化,因此如果需要,它将恢复它。如果您不支持最小化对话框,您可能不需要 SW_RESTORE 操作。另一方面,有时不实际销毁窗口而只是隐藏它会很方便,在这种情况下,您可以使用 SW_SHOW 作为操作。
为什么不只创建一个 CMyToolbox 变量(而不是引用)?因为您需要知道何时删除对象,并且如果您完全一致,从不在堆栈上或作为类成员分配,而只使用指向堆分配版本的指针,那么会更容易。您需要添加一个 PostNcDestroy 处理程序,这是一个虚拟方法,到您的类中,形式为
void CMyToolbox::PostNcDestroy() { CDialog::PostNcDestroy(); // add this line delete this; }
这保证了当窗口关闭时,窗口实例将被删除。*请注意,这不会更改您的指针,在我们的示例中,tools,因此除非您显式将其设置为 NULL,否则您将陷入严重的麻烦!*
我通过多种方式处理此问题。我使用的最常见方法是将一个用户定义消息发布回父窗口,告知无模式对话框已被销毁。这告诉 CWinApp 类它可以将变量清零。请注意,CWinApp,虽然是 CCmdTarget,但*不是* CWnd,因此您不能使用 PostMessage 向其发布消息。相反,您必须执行 PostThreadMessage 并执行 ON_THREAD_MESSAGE 或 ON_REGISTERED_THREAD_MESSAGE 来处理它。如果您不知道我在说什么,请阅读我关于消息管理的文章.
void CMyToolbox::PostNcDestroy() { CDialog::PostNcDestroy(); // add these lines delete this; AfxGetApp()->PostThreadMessage(UWM_TOOLBOX_CLOSED, 0, 0); }
并在您的 CWinApp 类的类定义中添加
afx_msg LRESULT OnToolboxClosed(WPARAM, LPARAM);
并在您的 CWinApp 消息映射中添加
ON_THREAD_MESSAGE(UWM_TOOLBOX_CLOSED, OnToolboxClosed)
或
ON_REGISTERED_THREAD_MESSAGE(UWM_TOOLBOX_CLOSED, OnToolboxClosed)
以及实现方法
LRESULT CMyApp::OnToolboxClosed(WPARAM, LPARAM) { tools = NULL; return 0; }
我在我写的文章中解释了普通消息和注册消息的区别。关于消息管理的文章。这有一些危险,因为如果您的应用程序恰好处于主消息循环之外的消息循环中(例如,有一个模态对话框或 MessageBox 处于活动状态),则 PostThreadMessage 将不会被看到,您必须在 MainFrame 类中处理 PostMessage。
优化和正确性
在 Win16 时代,GDI 资源稀少且宝贵。它们被小心地囤积。程序员竭尽全力避免耗尽 GDI 资源。这意味着如果您创建了一个字体,您就创建一次,尽可能多地使用它,并在程序终止时删除它。
这曾经是一项伟大的优化,并且当时是必需的。在现代 Win32 系统中,这实际上并不是一个好主意。原因是它违反了抽象。如果我创建一个需要特殊字体的控件,我应该*为该控件*创建该字体,而不是要求程序员为我创建。因此,CFont 对象位于控件子类中,而不是全局变量或 CDialog 类或 CWinApp 类的变量中。如果*所有*控件实例共享相同的字体需求,您可以通过使用静态类成员来保存 CFont 来简化一些事情,尽管您可能必须对其进行引用计数以知道何时删除它。
导致程序不正确或可能不正确的优化*不是*优化。小心!因为一些“节省空间”的优化的后果实际上可能会*泄漏*空间。这不好
更改控件中的字体
更改控件中的字体需要删除现有字体。这就是为什么使用只在一个控件中使用且不共享的字体是一个好主意。以下代码是正确的,并且不会在各地泄漏字体
void CMyControl::ChangeFont() { CFont f; f.CreateFont(...); CFont * oldfont = GetFont(); if(oldfont != NULL) oldfont->DeleteObject(); // Be Careful! See discussion below! SetFont(&f); f.Detach(); }
这样做的好处是每次更改字体时都不会泄漏 HFONT。如何指定 CreateFont 的参数留给读者作为练习;我发现我通常将大小、字体名称和粗体标志作为参数传递给我的 ChangeFont 方法,这涵盖了我更改控件中字体的大约 99%。在泛化上面的示例时,您必须弄清楚您的需求。
GetFont 返回一个 CFont 对象的引用,除非控件没有关联的字体,在这种情况下结果为 NULL。但是,如果结果非 NULL,我将通过调用 DeleteObject 来删除 HFONT。如果字体是我的字体之一,它现在已被删除(如果它是共享的,则该字体的其他用户会遇到麻烦——但我们稍后会讲到)。如果字体是内置字体,DeleteObject 无效。SetFont 然后设置字体,而 Detach,正如我刚才解释的,阻止 HFONT 随着 CFont 进入天堂。
哇!我们得到的那个 CFont* 怎么了?我们没有泄漏什么吗?不,因为 GetFont 创建的 CFont* 是一个*临时 MFC 对象*,这意味着它被添加到垃圾回收对象的列表中。下次出现空闲时间时,默认的 OnIdle 方法会遍历并删除所有临时对象。实际上,如果您说“delete oldfont”,您最终会从临时对象收集器那里得到一个 ASSERT 失败,它会告诉您您对它的数据做了一些坏事。
上述代码的一个注意事项
上述代码有一个 bug。它不是一个明显的 bug,但一位读者发现了它并指出了。我需要在此处更明确。您应该只对您创建的字体调用 DeleteObject。情况是这样的:读者做了和我展示的一模一样,并抱怨说尽管他的按钮现在有了所需的字体,但所有其他按钮现在都有了错误的字体。糟糕。这是我的错误。对话框中发生的情况是为对话框中的控件创建字体,并且在创建每个控件时都会进行 SetFont。
他所做的是,通过遵循我的建议,他设法删除了对话框字体,因此所有其他控件都落入了默认情况。正确的做法是,您应该只删除您创建的字体的旧字体,而不是控件的默认字体。您如何区分?好吧,如果您在 OnInitDialog 处理程序中,您就知道您没有创建字体,所以您不应该删除现有字体。我还没有检查过,但我相信对话框会在关闭时删除其对此字体的副本。
如果您以后要更改字体,并且不知道上次是否创建了字体,该怎么办?嗯,显而易见的方法是保留某种布尔值来表示您是否更改了字体。一个典型的例子是当您使用 CListBox 作为日志控件,并希望用户能够更改字体时。这不必要地复杂。当我不得不这样做时(请注意,在我写示例时我已经知道了!1 返回文本)我所做的是获取现有控件的 GetFont。然后我创建了一个与它相同的字体,并将*该*字体设置在控件中。代码如下所示
// Create a new font so we can change it later CFont * f = c_MyLogListBox.GetFont(); CFont newfont; LOGFONT lf; if(f != NULL) { /* Set up duplicate font */ f->GetObject(sizeof(LOGFONT), &lf); newfont.CreateFontIndirect(&lf); } /* Set up duplicate font */ else { /* Use default font spec */ newfont.CreateStockObject(ANSI_VAR_FONT); } /* Use default font spec */ c_MyLogListBox.SetFont(newfont); newfont.Detach();
执行此操作后,您可以安全地使用我之前的示例来删除旧字体,因为您知道它是您自己创建的。
感谢 Michael Mueller 花时间指出这个问题,并向他致以翻车鱼鳍的挥动。
将句柄附加到包装器:Attach()
Detach 有一个对应的操作,那就是 Attach。Attach 操作接受一个适当类型的 HANDLE 参数,并将其句柄附加到一个现有的 MFC 对象。MFC 对象不能已经附加了一个句柄。
因此,如果我购买了一个第三方 DLL,它告诉我其中一个函数返回一个 HFONT、HWND、HPEN 或任何其他句柄,我就可以使用 Attach 将该对象附加到一个已经存在的相应 MFC 对象。假设我有一个 DLL,它有一个名为 getCoolPen 的操作,用于它想要执行的操作。它返回一个 HPEN,我可以存储它。但将它存储为 MFC 对象可能对我很方便。一种方法是,例如,在 CView 派生类中,声明一个成员变量(可能是保护成员变量),
CPen myCoolPen;
然后我就可以这样做
void CMyView::OnInitialUpdate() { // ... myCoolPen.Attach(getCoolPen()); // ... }
请注意,这需要您了解 getCoolPen 调用的含义。如果 DLL 编写者已正确记录了产品,它将明确说明您是否必须在完成后删除 HPEN,或者*不能*删除 HPEN,因为它被共享。通常,此类有用信息只能通过阅读空中猪形生物的侧面来确定。但让我们假设我们确实知道应该做什么。
在您*必须*删除 HPEN 而不再需要它时,您无需执行任何特殊操作。当视图被销毁时,其所有成员的析构函数都会被调用,这意味着 CPen 的析构函数会被调用,从而删除底层的 HPEN。
在您*不得*删除 HPEN,因为它被共享的情况下,您必须在 CView 派生类的析构函数中添加一行,如下所示
CMyView::~CMyView() { // ... myCoolPen.Detach(); // ... }
创建对象:FromHandle
所有包装器类都支持一个额外的操作,即 FromHandle 方法。这是一个包装器类的*静态*方法,它接收底层 Windows 对象的句柄作为输入参数,并返回一个临时包装器对象作为结果。永久对象是指在空闲时间*不会*被垃圾回收的对象。
因此,如果我只执行 GetFont,我就会得到一个临时对象的引用。这个指针不能被存储,因为最终它占用的空间将被回收。以下代码存在致命缺陷
class CMyClass : public CView { protected: CFont * myFont; }; // anywhere in the class implementation myFont = GetFont(); // or myFont = CFont::FromHandle(...);
稍后使用 myFont 变量的尝试很有可能以一种非常有趣且灾难性的方式失败。也许是 ASSERT 失败,或者访问冲突,或者仅仅是行为不正确,例如没有明显字体更改。这是因为对象是由 GetFont 创建的,被添加到临时对象列表中,后来又被删除。当临时对象被删除时,底层 Windows 对象*不会*被删除,因为临时对象仅被视为代理。
存储底层对象引用的正确方法如下
CFont * f = GetFont(); if(f == NULL) myFont = NULL; else { /* attach it */ myFont = new CFont; myFont->Attach(f->m_hObject); } /* attach it */
请注意,这假设 myFont 要么是 NULL,要么其值无意义。如果它非 NULL,那么它很可能已经包含一个有效的 CFont 引用。您必须决定是否应该删除该引用,以及如果删除该引用,底层 HFONT 应该发生什么。只有当 myFont 变量尚未持有临时对象的引用时,才能执行此操作。在上面的示例中,由于我每次都创建一个新的 CFont,所以我知道它不是临时对象。两种可能的算法是
if(myFont != NULL) delete myFont; // delete object and HFONT
或者,或者
if(myFont != NULL) { myFont->Detach(); // leave HFONT alone! delete myFont; myFont = NULL; }
不要忘记在类的构造函数中将 myFont 成员设置为 NULL!
如果您碰巧删除了一个已经是临时对象的对象,MFC 将会从深处抛出一个断言失败,甚至可能是访问冲突,因为它试图删除已分配的临时对象。切勿删除临时对象。因此,以下代码是致命的
CFont * f;
f = somewindow.GetFont();
delete f;
如果您收到此消息,您将几乎立即知道您删除了一个临时对象;在返回到主消息循环后不久,您就会收到一个断言失败。
Windows:FromHandle 和 FromHandlePermanent
对于 CWnd 派生类,您可以使用 FromHandle 获取一个临时的 CWnd* 对象。因此,如果您有一个类 CMyCoolWindow,并且您执行了 GetFocus 之类的操作,您可能会或可能不会获得您 CMyCoolWindow* 对象的实际指针。我曾多次因此失败。例如,我养成了这样做
CWnd * capture = GetCapture(); if(capture != NULL && capture->m_hWnd == m_hWnd) { /* I have capture */ // ... do something } /* I have capture */
如果您需要给定 HWND 的实际对象的句柄,您应该使用 CWnd::FromHandlePermanent。这将从永久窗口映射返回一个句柄。请注意,CWnd::FromHandle*可能*返回一个永久窗口的句柄,也*可能*不返回。您没有保证。
CWnd 和线程
对象映射是线程本地的。这意味着如果您在一个线程中执行 CWnd::FromHandle,您将获得一个新的、临时的窗口对象,它*不是*最初代表您的类的同一个 C++ 对象。因此,在线程中这样做总是致命的
CMyCoolWindowClass * me = (CMyCoolWindowClass *)CWnd::FromHandle(hWnd); me->MyCoolVariable = 17; // modifies some location you've never heard of!
您实际上会得到一个通用的 CWnd 指针,如果您这样做
me->IsKindOf(RUNTIME_CLASS(CMyCoolWindowClass))
您将得到 FALSE。如果您这样做
CMyCoolWindowClass * me = (CMyCoolWindowClass *)CWnd::FromHandlePermanent(hWnd);
您将始终得到 NULL,因为线程的永久句柄映射是空的,除非您实际上是在该 UI 线程中创建了窗口。
如果您需要在线程(特别是工作线程)中访问窗口类,请通过线程例程的初始指针将其传递给线程。请参阅我关于工作线程以获取更多详细信息。
摘要
虽然本文主要关注 CFont 对象,但这里的技术适用于所有包装 Windows 对象的 MFC 类。CGDIObject,CPen、CBrush、CFont、CRgn、CPalette 等的超类,是实现 Attach、Detach 和 FromHandle 的地方。子类如 CPen 会覆盖 FromHandle 以接受 HPEN 并返回 CPen*,但实际上它们只是调用超类来完成所有工作,并提供必要的类型转换,以在 C++ 环境中正常工作。此外,CWnd 类还具有 Attach、Detach 和 FromHandle。CWnd 类还有另一个操作,FromHandlePermanent,我可能有一天会写,但现在不行。
所有这些操作都旨在让您在 Windows 对象域(其中对象由 HANDLE 实例表示)和 MFC 对象域(其中对象由 C++ 类的类实例表示)之间自由移动。理解这两种表示之间的关系以及如何以安全且不泄漏的方式使用它们,对您非常有帮助。
脚注:(返回文本)
1 好吧,我忍不住了。我每次讲课都会讲这个故事,因为它捕捉到了知识交流的一个问题。
多年前,在一个任意选择的东方文化中,一位年轻的学生来到一位伟大的武术大师面前,说他想成为他的弟子。“这不是一个容易的选择,孩子;你需要学习很多年才能成为一位伟大的大师。”年轻人坚持不懈,说他明白承诺,并在接下来的 20 年里学习成为一名大师。最后,伟大的大师对他说:“我的儿子,你现在知道我所知道的一切了。去吧,收一个自己的学生吧!”这时,学生心想,啊哈!我知道他所做的一切。我知道每一个动作和反击!我能打败他!
几个小时后,学生醒了过来。伟大的大师正俯视着他,摇着头说:“啊,忘了那个把戏!” (返回文本)
这些文章中表达的观点是作者的观点,不代表,也不被微软认可。