1/24/2005

IStream接口和CString之间的转换

问:如何传递CString中包含的字符串到具有IStream类型参数的函数?
问:如何根据获得的IStream接口指针获得字符串?
答:
MFC7中的未归档类CStreamOnCString实现了IStream接口访问封装的CString类型的m_strStream成员。如果你没有MFC7,可以用CreateStreamOnGlobal创建IStream的内存流实现对象,之后调用IStream::Write将字符串写入内存流对象;或者创建IStream的内存流实现对象,反复调用IStream::Read和IStream::Write从源流复制数据到内存流,然后调用GetHGlobalFromStream函数直接访问内存流的内存句柄。

上面的一些调用在MFC中被封装到COleStreamFile 类中。
下面的代码把一个内存流读到字节数组。你可以根据字符串的类型把字节数组转化成字符串。
COleStreamFile osfRead;
osfRead.Attach(pStream);
long lLength=osfRead.GetLength();
CByteArray baBuf;
baBuf.SetSize(lLength);
osfRead.Read(baBuf.GetData(),lLength);

何时一个类的指针可以强制转化为另外一个类的指针,即使它们之间没有派生关系?

问:我看到CListView的成员函数GetListCtrl直接把CListView本身的指针转换为CListCtrl指针。我想知道在什么情况下可以安全地把一个类的指针转化为另一个类的指针?
答:只要你访问的数据的内存表示是完全相同的,那么这种转化就是安全的。考虑MFC从CWnd到CEdit的派生过程,是一个纯粹的封装——既没有增加数据成员也没有增加虚函数(隐含的虚函数表的指针实际上是类的一个成员,你可以在VC的监视窗口看到)——所有的代码只是在操作基类的成员。所以你可以在对话框的成员函数中安全地进行如下调用
CEdit* pEdit=GetDlgItem (IDC_EDIT);//把CTempWnd类型的对象的指针转化成CEdit指针
也就是说,你可以安全地把CWnd指针转化为CEdit指针。在CWnd的窗口类为EDIT的情况下,你可以安全地用这个CEdit指针来访问CEdit具有的而CWnd不具有的方法。由于CEditView的窗口类也是EDIT,同时是从CWnd派生下来的,所以你也可以把CEditView指针转化成CEdit指针。MFC的CCtrlView的派生类都可以做这样的转化。
在这样做之前,你需要查看类的定义头文件以确保类型安全。CEdit, CListCtrl, CTreeCtrl,CToolBarCtrl都是这样的封装,但是CToolTipCtrl是一个例外。除了对TTM_ADDTOOL消息的处理可能不一致之外。
LRESULT CToolTipCtrl::OnAddTool(WPARAM wParam, LPARAM lParam)
{
 TOOLINFO ti = *(LPTOOLINFO)lParam;
 if ((ti.hinst == NULL) && (ti.lpszText != LPSTR_TEXTCALLBACK)
  && (ti.lpszText != NULL))
 {
  void* pv;
  if (!m_mapString.Lookup(ti.lpszText, pv))
   m_mapString.SetAt(ti.lpszText, NULL);
  // set lpszText to point to the permanent memory associated
  // with the CString
  VERIFY(m_mapString.LookupKey(ti.lpszText, ti.lpszText));
 }
 return DefWindowProc(TTM_ADDTOOL, wParam, (LPARAM)&ti);
}
这里访问了CToolTipCtrl的数据成员,这样的话下面的代码看起来就似乎未必安全
_AFXCMN_INLINE CToolTipCtrl* CTreeCtrl::GetToolTips() const
 { ASSERT(::IsWindow(m_hWnd)); return (CToolTipCtrl*)CWnd::FromHandle((HWND)::SendMessage(m_hWnd, TVM_GETTOOLTIPS, 0, 0L)); }
不过CTreeCtrl::GetToolTips()返回的只是有效的CWnd,TVM_ADDTOOL消息也是被默认窗口过程而不是CToolTipCtrl的代码来处理, 所以上面的函数没有机会执行,这个用法还是安全的。
顺便说一下,尽管大部分这样的封装是用SendMessage实现的,但是Redmond的家伙似乎很热衷于用DefWindowProc替代SendMessage来节省CPU的时钟周期。这样的一个副作用是MFC不会有处理这样的消息的机会,例如PretranslateMessage和Subclass都会失效了。

强制转换的一个副作用是protected修饰符不再是类成员的保护伞。考虑如下情况
Class A
{
    protected: int m_iProtected;
};
如果我想获得m_iProtected的public访问权,我可以写一个封装类
Class B:public A
{
public:int & GetProtected(){return m_iProtected;}
friend class C;
};
然后把A类型的指针强制转化成B类型的就可以以public方式访问声明为m_iProtected的成员了。在C类中访问B的成员也不受限制。


尽管多继承是C++的一个特性,从理想情况来说,CEditView应该从CCtrlView和CEdit派生出来;但是所有的人都知道MFC中的多继承是多么的麻烦。对于MFC的CObject派生类,默认的情况是不允许的——尽管你可以通过的MFC技术文档TN016: Using C++ Multiple Inheritance with MFC中提供的方法手动添加多继承支持。实际上,这些特性应该以接口的形式提供——把GetEditCtrl转换成QueryInterface,但是这样的话性能上又会损失很多,而且Windows通用控件和MFC对它的封装总是在不断地升级——接口的噩梦就是升级——所以MFC使用了如上的”黑客”方法来提供和多继承类似的功能。
关键字:MFC "no data members"
参考
C++ Q & A -- Microsoft Systems Journal May 1998

虚析构函数

编译器总是根据类型来调用类成员函数。但是一个派生类的指针可以安全地转化为一个基类的指针。这样删除一个基类的指针的时候,C++不管这个指针指向一个基类对象还是一个派生类的对象,调用的都是基类的析构函数而不是派生类的。如果你依赖于派生类的析构函数的代码来释放资源,而没有重载析构函数,那么此时会有资源泄漏。

所以建议的方式是将析构函数声明为虚函数。如果你使用MFC,并且以CObject或其派生类为基类,那么MFC已经为你做了这件事情;CObject的析构函数是虚函数。一个函数一旦声明为虚函数,那么不管你是否加上virtual修饰符,它在所有派生类中都成为虚函数。但是为了理解明确起见,建议的方式还是加上virtual 修饰符。

C++不把虚析构函数直接作为默认值的原因是虚函数表的开销以及和C语言的类型的兼容性。有虚函数的对象总是在对象开始或者末尾的位置(具体位置随编译器而异)包含一个隐含的虚函数表指针成员。对于MFC类CPoint和CSize这样的小型类,增加一个指针成员就增加了很多内存占用,而且使得其内存表示和基类POINT和SIZE不一致。如果两个类的内存表示一致,那么你有时可以安全地把一个类的指针或数组当作另一个类的指针或数组使用。

如果你声明了一个虚析构函数而没有实现它,那么编译器通常会在连接时报告错误——就像其它的虚函数没有实现时一样,但是有的编译器没有检测这个问题,这样的话在对象析构时会造成运行时崩溃。推荐的方案是在析构函数的声明之后,在类声明中嵌入实现。

1/23/2005

在Visual C++中编译工程时自动增加版本号

微软知识库中的文章How To Increment Version Information After Each Build in Visual C++(http://support.microsoft.com/kb/237870/)提供了在VC6中自动增加版本号的方法。在VS.Net中,需要对这个宏进行少许的更改:http://www.thecodeproject.com/macro/IncBuildNrMacro.asp?df=100&forumid=49502&select=1008554#xx1008554xx

也可以使用VS.NET插件来实现该功能,一个示例在下面的网址可以找到
An Automatic Build Number Incrementer for Visual Studio .NET
http://codeguru.earthweb.com/Csharp/.NET/net_vs_addins/article.php/c5961/

但是宏比插件更加灵活,更稳定,更便于修改和扩展。

编程控制Modem/PPPoE拨号连接

在Windows中拨号上网(包括MODEM和PPPoe),一般都是用Windows平台提供的的Remote Access Service(RAS,远程访问服务):http://msdn.microsoft.com/library/en-us/rras/rras/ras_start_page.asp 。其中的连接操作函数(http://msdn.microsoft.com/library/en-us/rras/rras/ras_connection_operations.asp)可以用于对拨号连接进行操作。比较常用的几个函数是RasDial、RasHangUp、RasEnumConnection和RasGetConnectStatus,以及自定义的通知处理函数(http://msdn.microsoft.com/library/en-us/rras/rras/notification_handlers.asp)。

Windows XP/2003内建了对PPPoe拨号连接的支持,参考http://msdn.microsoft.com/library/en-us/rras/rras/point_to_point_protocol_over_ethernet_connections.asp 。

一个控制拨号连接的代码示例可以在http://www.pconline.com.cn/pcedu/empolder/gj/vc/10309/221591.html 找到(原始出处搜索半天没搜到……)。

编程实现远程唤醒PC

为了唤醒网络上的计算机,必须发出一种特殊的数据包,该数据包的格式与普通数据包不同,而且还必须使用相应的专用软件才能产生。当前普遍采用的是AMD公司制作的Magic Packedt这套软件以生成网络唤醒所需要的特殊数据包,俗称魔术包(Magic Packet)。该数据包包含有连续6个字节的“FF”和连续重复16次的MAC地址。
Magic Packet格式虽然只是AMD公司开发推广的技术,并非世界公认的标准,但是仍然受到很多网卡制造商的支持,因此 许多具有网络唤醒功能的网卡都能与之兼容。

需要更多关于Magic Packet的信息的话,可以参考

http://www.amd.com/us-en/ConnectivitySolutions/TechnicalResources/0,,50_2334_2481,00.html

一个生成网络唤醒所需要的特殊数据包的示例代码可以在http://staff.aist.go.jp/d.g.fedorov/ether-wake.c 找到。

更多信息可以参考http://blog.mllm.org/index.php?q=node/59

1/22/2005

限制应用程序的实例数目

某些应用程序处理紧缺资源,例如可擦写光驱、串口或者大量内存,通常不希望这种应用程序的多个实例同时运行。

实际上你没有办法限制用户只能启动一次。你可以做到的是在应用程序启动之后查找是否用户启动了另一个实例。如果没有找到现存的实例,应用程序以正常方式启动。否则,通常的处理是退出。

要查找现存的实例,可以查找进程列表,比较每一个进程的文件名称和本应用程序的文件名称,如果找到了和文件名匹配的进程,那么认为应用程序已经启动了(参考平台SDK的Spy示例和Visual C++的TList示例)。这种方法的缺点是需要额外的工作(例如枚举找到的进程的窗口)来传递命令行参数。关于如何枚举一个进程或者线程的窗口,可以参考GetWindowThreadProcessId。也可以用约定的暗号来识别,例如创建一个对象,具有很难重复的属性,然后查找系统中是否已经有了同样属性的对象。通常创建互斥体(MUTEX)或者具有指定窗口类的窗口(不适用于对话框,因为对话框的窗口类是#32770)。一些其他的方法包括检查窗口过程处理特定消息的返回值、检查窗口的约定属性等等。建议用互斥体对创建对象的代码加锁,以避免同时创建两个对象。

有的程序在退出之前需要把命令行参数传递给现存的实例,例如一些文档处理程序和浏览器。官方的方法是在注册表中对应的文件类型中注册DDL命令:HKCR/filetype/shell/open/ddeexec = Open("%1")。 遗憾的是,MFC只在CFrameWnd类里面实现了对DDE的封装。如果你的程序不基于文档视图框架,那么实现DDE支持是很麻烦的事情。幸好你不必这么做,比较简单的方法是发送一个消息——例如WM_COPYDATA——到找到的现存实例窗口,传递命令行参数作为参数。

有的程序,例如IE或者资源管理器,Office等,使用多顶层窗口来替代应用程序的多个实例。尽管MFC7的应用程序向导提供了多顶层窗口的选项,但是这样的实现是不完美的——所有顶层窗口仍然在同一个线程内,使得每个顶层窗口没有单独的状态。举例来说,如果一个浏览器窗口显示了一个消息框,那么其他顶层窗口也会挂起。使用腾讯浏览器的人可能对这种情况很是熟悉。

MFC示例MTMDI演示了如何在MDI程序中在单独的线程中创建窗口,以及相应修改消息和命令传递过程。虽然这样应用程序改动最少,但是这打破了DOC/View架构,而且顶层窗口还是在一个线程中。
用对话框作为顶层窗口是最简单的,在单独的线程中创建对话框也很容易,但是在对话框上创建工具栏要编写一些额外的代码来模仿框架的行为,而且没有文档视图的支持,需要自己编写打开文档和网址的代码。还有一些其它的副作用,例如需要在对话框上实现工具栏、状态栏菜单的自动更新,不能通过自定义窗口类的办法查找现有的应用程序实例等等。

编写多顶层窗口文档处理程序时,在现存实例运行时,在资源管理器打开新的文件的实现是,新的实例发送命令参数到现存实例,创建一个新的顶层窗口之后退出。有很多种方法获得这个参数。MFC程序当然也是C++程序,所以标准C的argc和argv主函数入口参数仍然有效。但是MFC提供了命令行处理的封装类CCommandLineInfo,支持了标准的命令行开关的分析。但是更有效地处理命令行参数的办法还是采用MSJ1999年10月中的C++Q&A专栏的CCommandLineInfoEx类。它提供了自定义命令行开关分析的方法。

编程控制活动桌面,用ActiveX控件来增强桌面的功能

活动桌面处理和一个例子http://www.vckbase.com/vckbase/vckbase10/vc/nonctrls/atlcomocx_02/1002001.htm)讲述了使用IActiveDesktop接口可以做到的事情。活动桌面允许在桌面上显示HTML网页,这也意味着我们可以在桌面上的项目中以在网页中使用ActiveX控件来对网页进行扩展的方式来提供丰富的内容。但是不建议在桌面上使用不安全的控件,例如Windows Media Player 6.0。虽然用一段影片作为桌面背景是一件很酷的事情,但是你不得不忍受在每次切换任务或者刷新桌面的时候都弹出安全性提示对话框。你可以在编写自己的控件时利用
http://msdn.microsoft.com/library/CHS/vccore/html/_atl_step_7.3a_.putting_the_control_on_a_web_page.asp中的技术来避免这个烦人的对话框。