以下是引用片段:
struct blah {
int a, b;
char *a, *b;
struct blah *next;
};
struct blah *getmystruct();
因为 getmystruct() 分配了内存
,所以当我使用完这个函数后
,我需要调用 freemystruct(struct blah *b)
。我试图生成一个包装,将它转换成
托管类型的集合,但我不知道当这些指针需要释放时如何处理它们
。您能指点我一下吗?
Cory Nelson
答:哦,当然可以。您不能通过简单的 dllimport 语句将本机列表转换成
托管类型的集合。interop 服务不错,但不是非常 好!您需要编写一个包装,它将您的列表显式转换成托管集合(例如 ArrayLis)。我编写了一个具有三个模块的程序 (ListWrap) 来说明如何实现。第一个模块是 ListLib.cpp,它实现了一个具有两个函数(AllocateList 和 FreeList)的本机 C++ 库 (DLL),它们的作用是分配和释放本机 C++ 结构的链接列表。这两个函数模拟应用程序中的 getmystruct 和 freemystruct 函数。第二个模块是一个托管 C++ 文件 — ListWrap.cpp,它实现了一个包装本机 C++ 实现的托管类 ManagedNode(请参见图 3)。第三个模块是一个 C# 测试程序,它调用包装来显示如何工作。您可以从 MSDN Magazine Web 站点下载 ListLib.cpp 和 C# 测试程序的完整源代码。
ListLib.cpp 实现了两个本机函数 — AllocateList 和 FreeList,它们用于分配和释放 NativeNode 结构的列表:
以下是引用片段:
// from ListLib.h
struct NativeNode {
int a, b;
TCHAR *str;
struct NativeNode *next;
};
ListWrap.cpp 中的包装类 ManagedNode 模拟 NativeNode 的定义,不过有几个细微区别:本机 char* 被替换为托管 String,并且由于我使用 ArrayList 来实现列表结构,所以没有 next 指针。在代码中,它应该如下所示:
以下是引用片段:
// managed equivalent of NativeNodepublic __gc class ManagedNode {public: int a, b; String* str;};
定义好 ManagedNode 之后,下一步就是编写一些代码将 NativeNode 转换成 ManagedNode。但在开始编写之前,请稍微考虑一下转换器函数应该是什么样子的,它应该有什么样的参数和返回值。一种方式是编写一个函数,将 NativeNode 的本机列表作为参数并返回 ManagedNode 的托管列表,它可能销毁进程中的本机列表。.NET 客户端应用程序会直接调用 ListLib DLL(或者您的 getmystruct)来获取本机列表,将其作为 IntPtr,然后将这个 IntPtr 传递给转换函数,如下所示:
以下是引用片段:
// call DLL directly through interop
IntPtr nativeList = AllocateList(7);
// call wrapper to convert
ArrayList amanagedList = ListWrap.Convert(nativeList);
在大多数情况下,客户端要负责调用 DLL 来释放本机列表,或者由 Convert 函数自动完成释放过程。
另一种方法是完全隐藏 DLL,其做法是:将本机函数 AllocateList 包装在一个包装器中,由它分配列表、转换并释放原始本机列表,再将托管列表作为 ArrayList 返回。哪种方法更好呢?第一种策略的优点是您只需编写一个简单的转换函数,在任何具有本机列表的地方都可以使用它。第二种策略需要包装创建列表的每个函数。如果您有多个创建列表的函数,这种方式会有些麻烦,但是它的优点是对 .NET 客户端完全隐藏了所有本机内容。客户端不需要处理 IntPtrs,甚至不需要导入 DLL;ListWrap 将这一切都隐藏了。我采取的是这种方式,我也鼓励您在自己的应用程序中使用这种方式。它要对库进行完全包装还需要做很多工作,但结果会比较可靠,而且完全封装。
有了 ManagedNode 之后,剩下的工作就是包装 AllocateList 了。这个过程十分简单。首先,调用 AllocateList 来分配本机列表,然后创建一个空的 ArrayList。接下来将所有 NativeNode 复制到 ManagedNode,并将它们添加到托管列表中,您在进行这些操作时会将它们删除。图 3 显示了完整的细节。托管 C++ 的好处之一就是所有代码看起来很简单、整洁,即使您处理的是混合对象。将本机 char* 复制到托管 String 只是一个简单的赋值,如以下的代码行所示:
以下是引用片段:
mn->str = nn->str; // String = char*: it just works!
不需要调用转换函数,编译器知道如何去做。CreateList 在运行时会删除本机节点。这比在最后删除节省存储
空间。
通过将整个列表转换成托管对象(而不是通过 interop 和 StructLayout 将它导出),您可以为托管客户端营造一种托管环境。这叫入乡随俗!毕竟,一些程序员选择 .NET 的主要原因之一就是它具有自动垃圾回收的功能。如果您直接通过 interop 导出列表,您还必须导出 FreeList,并要求使用其他基于 .NET 语言的程序员要记得调用它。
一般情况下,如果您导出到托管环境,最好是将尽可能多的数据转换成托管对象。什么情况下例外呢?您的客户端也是采用 C++ 编写的。当然,这条规则并不总是适用。有时更好的方式是直接导出结构,并要求客户端释放它们 — 例如,如果复制对性能或内存造成的影响太大而不可接受,就需要这样做。您必须使用判断来决定是进入托管环境还是进入本机环境。
问:我正在使用 C++ 托管扩展来包装现有的 C++ 库,使基于 .NET 的语言可以访问它。在托管 C++ 中,我可以写为
以下是引用片段:
String* s = new String();
s = _T("Hello, world");
但如何将托管 String 再次转换为本机 TCHAR*?
Matthew Brady
答:一旦您了解了神奇的 voodoo,这会变得很简单。您必须调用 PtrToStringChars 并 pin 结果。代码如下所示:
以下是引用片段:
String __gc* s = S"Hello";
const wchar_t __pin* p = PtrToStringChars(s);
不要忘记对从 PtrToStringChars 返回的指针进行 __pin 操作。Pin 是必须的,因为 PtrToStringChars 将一个托管 (__gc) 指针返回给托管内存中的 String 对象的第一个字符,而垃圾回收器随时都会回收托管内存,除非您显式对它进行 __pin。一般情况下,每次将 __gc 指针传递给本机(非托管)函数时都必须使用 __pin。
图 4 显示了一个小程序,它将托管 String 转换成宽字符和 ANSI 字符串。要转换成 ANSI,可以使用自己喜欢的转换函数,如 wcstombs 或 ATL W2A 宏。如果您使用 MFC Cstrings,则不必进行任何操作,因为 CString 对 char* 和 wchar_t 都有赋值运算符:
以下是引用片段:
// both will work
CString s1 = "hello, world";
CString s2 = L"Hello, world";
问:在我的应用程序中,我想将选项卡控件的背景颜色从 gray 转换成 white。我试图从 CTabCtrl 派生一个类并使用所有的功能,但没有成功。能告诉我怎么办吗?
Mayur Patel
答:更改选项卡控件中选项卡的颜色十分简单,但要将属性表的颜色翻新则需要大量的工作,不下很大决心是做不到的。对于选项卡,基本思想是使控件成为所有者描述的控件,然后处理 WM_DRAWITEM。如果您使用 MFC,则可以重写虚函数 DrawItem。
在 Microsoft Systems Journal1998 年 3 月发行的那一期中,我介绍了如何实现一个选项卡控件类 — CtabCtrlWithDisable,它支持禁用选项卡。作为禁用选项卡的一部分,当选项卡禁用时,CTabCtrlWithDisable 将选项卡的文本颜色变为浅灰色。本月,我从 CTabCtrlWithDisable 借用了一些代码,实现了一个新的类 — CcolorTablCtrl,它可以使您更改选项卡的颜色(请参见图 5)。
要使用 CcolorTablCtrl,请在您的属性表中创建一个实例:
以下是引用片段:
class CMyPropSheet : public CPropertySheet {
protected:
CColorTabCtrl m_tabCtrl;
};
您必须在属性表的 OnInitDialog 处理程序中子类化选项卡控件(以便 MFC 可以使用它),然后将前景色和背景色设置成您喜欢的任何颜色:
以下是引用片段:
// in CMyPropSheet::OnInitDialog()
HWND hWndTab = (HWND)SendMessage(PSM_GETTABCONTROL);
m_tabCtrl.SubclassDlgItem(::GetDlgCtrlID(hWndTab), this);
m_tabCtrl.SetColor(WHITE, RED);
这里的 WHITE 和 RED 是标准的 COLORREF 值,即 RGB(255, 255, 255) 和 RGB(255,0,0)。一旦您实例化并初始化 CcolorTabCtrl,颜色选项卡控件就会完成剩下的工作(请参见图 6)。
图 6 颜色选项卡控件
CColorTabCtrl 重写了 SubclassDlgItem,它调用 ModifyStyle 来将风格更改为TCS_OWNERDRAWFIXED。进行重写的较好位置是在 PreSubclassWindow 中,因为不管控件是子类化还是通过 CreateWindow 创建都会调用这个函数(但由于杂志篇幅有限,我必须对代码进行压缩,因此我采用了这样的捷径)。注意,SubclassDlgItem 是一个简单的重写,而不是虚函数。为了设置颜色,SetColor 将颜色保存在两个成员变量 m_clrBackground 和 m_clrForeground 中。
一旦风格设置为所有者描述的,每次需要 Windows? 绘制选项卡时,它都会发送 WM_DRAWITEM 消息。MFC 捕获这个消息并调用选项卡控件的虚函数 DrawItem,CColorTabCtrl 通过用新的颜色绘制文本来实现这个函数:
以下是引用片段:
// in CColorTabCtrl::DrawItem
dc.FillSolidRect(rc, m_clrBackground);
dc.SetBkColor(m_clrBackground);
dc.SetTextColor(m_clrForeground);
dc.DrawText(...);
这些都非常简单,所以要了解具体实现请查看源代码。由于您可能不想只更改选项卡颜色而不更改页面颜色,所以我也实现了一个 CcolorPropertyPage,它可以让您将属性页的背景颜色改成匹配的颜色,如图 6 所示。对于属性表,更改背景颜色最简单的方式就是处理 WM_ERASEBKGND:
以下是引用片段:
BOOL CColorPropertyPage::OnEraseBkgnd(CDC* pDC)
{
CRect rc;
GetClientRect(&rc);
pDC->FillSolidRect(rc, m_clrBackground);
return TRUE;
}
如果您自己试运行这段代码,您会发现有各种令人苦恼的问题。首先,如果您更改页面颜色,所有控件的背景色都会是错误的,所以还必须修正。对此,您必须处理 WM_CTLCOLOR 和 WM_ERASEBKGND。有关详细信息,可以参阅 1997 年 5 月一期中我的 MSJ 专栏。
另一个问题是选项卡控件仍然使用系统 3D 颜色来绘制选项卡的边缘和圆角。唉!要修正这个问题,只能自己处理 WM_PAINT 并负责所有的绘图操作。包括画出选项卡在被选定时与其他选项卡的偏移,以便它以前景色显示。此时,您就要开始对选项卡控件进行一番彻底改造了。每个使用 Windows 的
编程人员都知道,更改控件颜色无一例外都是很痛苦的事情,一旦您走上了这条路,要做的事情就似乎没有尽头了。相信很快标准颜色就会比它们原先的颜色漂亮许多,否则您会产生疑问,为什么不转为使用 .NET Framework,它要更改颜色只需简单地写成:
以下是引用片段:
ctl.BackColor = Color.Aquamarine;
祝大家
编程愉快!