💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# Part IX - GDI Classes, Common Dialogs, and Utility Classes 原作 :[**Michael Dunn**](http://www.codeproject.com/wtl/WTL4MFC9.asp) 翻译 :[Orbit(桔皮干了)](http://www.winmsg.com/cn/orbit.htm) ## 本章内容 * [介绍](#intro) * [GDI 封装类(wrapper classes)](#gdiwrappers) * [封装类的通用函数(Common functions)](#wrapperscommon) * [使用 CDCT](#usingcdct) * [与MFC封装类的不同之处](#mfcdifferences) * [资源装载(Resource-Loading)函数](#resourceloading) * [使用公共对话框](#usingcommdlg) * [CFileDialog](#usingcfiledialog) * [CFolderDialog](#usingcfolderdialog) * [其它有用的类和全局函数](#miscstuff) * [结构封装](#structwrappers) * [双类型参数的自适应类](#dualtypedargs) * [其它工具类](#otherclasses) * [全局函数](#globalfuncs) * [宏](#macros) * [例子项目](#samplecode) * [版权和许可](#copying) * [修订历史](#revisionhistory) ## 对第九部分的介绍 WTL还有很多封装类和工具类在本系列文章前八篇中并没有介绍,例如CString和CDC,WTL还提供了对GDI对象的良好封装,还包括一些有用的资源装载函数以及WIN32通用对话框的封装类,使得通用对话框更加容易使用。现在,在本文的第九篇中,我将介绍一些最常用的工具类。 本文将讨论以下内容: 1. GDI 封装类 2. 资源装载(Resource-loading)函数 3. 使用打开文件和选择文件夹的通用对话框 4. 其它有用的类和全局函数 ## GDI 封装类 WTL 使用了与MFC截然不同方式封装GDI对象,WTL的方法是为每种GDI对象设计一个模板类,每个模板都有一个名为t_bManaged的bool类型模板参数,这个参数控制着这些类是否“管理”(或拥有)它所封装的GDI对象。如果t_bManaged是false,表示这个C++对象并不管理GDI对象的生命周期,它只是围绕着这个GDI对象句柄的简单封装;如果t_bManaged是true,就产生了两点不同之处: 1. 如果封装的句柄不为NULL,析构会调用`DeleteObject()`函数释放资源。 2. 如果封装的句柄不为NULL,`Attach()` 会在捆绑其它新句柄之前调用`DeleteObject()`释放当前封装的句柄 这种设计与ATL窗口类的封装风格是一致的,ATL的CWindow就是对HWND句柄的一个简单的封装类,而CWindowImpl则负责管理窗口的整个生命周期。 GDI的封装类都定义在_atlgdi.h_中(译者注:注意是“定义在”而不是通常说的“声明在”头文件中,这是因为ATL/WTL使用的是包含编译模式,所有的代码都在头文件中), 只有`CMenuT`例外 ,它定义在_atluser.h_中。你不需要直接包含这些头文件,因为它们已经包含在_atlapp.h_ 中了。每种模板类都由很容易记忆的名字: | 封装 GDI 对象 | 模板类 | 可管理对象封装 | 简单的句柄封装 | | --- | --- | --- | --- | | Pen | `CPenT` | `CPen` | `CPenHandle` | | Brush | `CBrushT` | `CBrush` | `CBrushHandle` | | Font | `CFontT` | `CFont` | `CFontHandle` | | Bitmap | `CBitmapT` | `CBitmap` | `CBitmapHandle` | | Palette | `CPaletteT` | `CPalette` | `CPaletteHandle` | | Region | `CRgnT` | `CRgn` | `CRgnHandle` | | Device context | `CDCT` | `CDC` | `CDCHandle` | | Menu | `CMenuT` | `CMenu` | `CMenuHandle` | 相对于MFC围绕着对象指针封装的方法,我更喜欢WTL的方法:你不用总是担心得到一个NULL指针(封装的句柄也可能是NULL,但这是另一回事), 也不用总是注意操作临时对象这种特殊情况(译者注:MFC有一个回收机制,总是试图释放使用FromHandle得到的临时对象),还有一点就是使用这种形式封装的类实例只占用很少的资源,因为类只有一个成员变量,就是其封装的句柄。象这种类似CWindow的封装形式,你可以在不同的线程之间传递封装类对象而不用担心线程安全问题,正是因为这样,WTL也不需要象MFC那样的线程特殊性映射。 下面的设备上下文封装类用于一些特殊的绘制场景: * `CClientDC`: 封装了GetDC()和ReleaseDC(),用于Windows客户区的绘制`。` * `CWindowDC`:封装了 `GetWindowDC()` `和ReleaseDC(),用于在整个Windows上下文中绘制。` * `CPaintDC`: 封装了`BeginPaint()` 和` EndPaint()`,用户`WM_PAINT` 消息响应函数。 每个类的构造函数都有一个HWND(窗口句柄)参数,类的行为和MFC的同名类相似。三个类都是CDC的派生类,它们自己管理设备上下文。 ## 封装类的通用函数 所有的GDI封装类使用的是相同的设计理念,所以它们的使用方法都大同小异,为了简单起见,这里只介绍一下CBitmapT类。 封装 GDI 对象句柄 每个类都由一个公有成员变量,也就是C++对象封装的GDI对象句柄,CBitmapT有一个HBITMAP类型成员,名为`m_hBitmap。` 构造函数 构造函数有一个HBITMAP类型的参数,默认值是NULL,`m_hBitmap`将被初始化位这个值。 析构函数 如果`t_bManaged` 是true,并且`m_hBitmap`不是NULL,那么析构函数会调用`DeleteObject()`释放这个bitmap。 `Attach()` 和 `operator =` 这两个函数都有一个HBITMAP类型的参数,如果`t_bManaged`是true,并且`m_hBitmap`不为NULL,它们会调用`DeleteObject()`释放这个`CBitmapT`对象管理的bitmap,然后将`m_hBitmap`的值设为作为参数传递进来的那个HBITMAP。 `Detach()` `Detach()` 将`m_hBitmap`的值设为NULL,然后返回`m_hBitmap`的值,`Detach()`调用以后,`CBitmapT`对象就不再关联GDI bitmap了。 创建 GDI 对象的函数 `CBitmapT` 封装了几个用来创建位图的WIN32 API :`LoadBitmap()`,`LoadMappedBitmap()`、`CreateBitmap()`、`CreateBitmapIndirect()`、`CreateCompatibleBitmap()`、`CreateDiscardableBitmap()`、`CreateDIBitmap()`、`CreateDIBSection()`。 这些方法将保证`m_hBitmap`不是NULL然后返回一个,如果要将这个`CBitmapT`对象用于其它GDI bitmap,需要首先调用`DeleteObject()` 或`Detach()` 函数。 `DeleteObject()` `DeleteObject()` 销毁GDI bitmap对象,将`m_hBitmap`设为NULL,调用这个函数时`m_hBitmap`应该不为NULL,否则将会出现断言错误。 `IsNull()` `如果m_hBitmap不为NULL,IsNull()` 返回true,否则返回false。使用这个函数可以测试CBitmapT对象是否关联了一个GDI bitmap。 `operator HBITMAP` 这个转换操作符返回`m_hBitmap`,这样你就可以将`CBitmapT` 对象传递给那些使用HBIMAP句柄作为参数的函数或Win32 API。 这个转换操作符还可用在测试这个对象合法性的布尔表达式中,其值与`IsNull()`刚好相反。例如,下面两个if语句时等价的: ``` CBitmapHandle bmp = /* some HBITMAP value */; if ( !bmp.IsNull() ) { do something... } if ( bmp ) { do something more... } ``` `GetObject()` 封装 `CBitmapT`对Win32 API `GetObject()`有一个类型安全的封装:`GetBitmap()`,它有两个重载形式,一个使用`LOGBITMAP*` 类型的参数并直接调用`GetObject()`;另一个使用`LOGBITMAP&` 类型的参数并返回一个bool值表示操作是否成功,后一个版本比较容易使用,例如: ``` CBitmapHandle bmp2 = /* some HBITMAP value */; LOGBITMAP logbmp = {0}; bool bSuccess; if ( bmp2 ) bSuccess = bmp2.GetLogBitmap ( logbmp ); ``` 对于操作GDI 对象的API的封装 `CBitmapT` 封装了操作`HBITMAP` 的API:`GetBitmapBits()`、`SetBitmapBits()`、`GetBitmapDimension()`、`SetBitmapDimension()`、`GetDIBits()` 和 `SetDIBits()`,这些函数都对`m_hBitmap`是否是NULL进行断言。 其它有用的方法 `CBitmapT` 有两个很有用操作`m_hBitmap`的函数:`LoadOEMBitmap()` 和 `GetSize()。` ### Using CDCT `CDCT` 与其它类稍有不同,所以这里单独介绍一下这个类。 #### 方法有哪些不同 销毁DC时调用`DeleteDC()` 而不是`DeleteObject()`。 #### 将对象选入DC MFC的CDC类一大诟病就是给DC选入设备时容易出错,MFC的CDC类对SelectObject()函数有几个不同的重载形式,每种重载都使用一个指向不同GDI封装类的指针作为参数,(`CPen*`, `CBitmap*`, 等等)。如果你传递一个C++对象而不是对象指针给SelectObject()函数,代码最终将调用一个使用`HGDIOBJ`作为参数的重载形式(这个重载形式并未见诸于正式文档),这将导致错误发生。 WTL的 `CDCT` 采用一种稍好一点的方法,它的几个select函数都是直接使用对应类型的GDI对象: ``` HPEN SelectStockPen(int nPen) HBRUSH SelectStockBrush(int nBrush) HFONT SelectStockFont(int nFont) HPALETTE SelectStockPalette(int nPalette, BOOL bForceBackground) ``` ### 与 MFC 封装类的不同之处 **较少的构造函数:** 这些封装类缺少创建新GDI对象的构造函数,例如:MFC的CBrush类可以创建使用实心填充方式和模式填充方式的画刷对象。在WTL中,你必须调用创建方法来创建一个GDI对象。 **向DC选入对象做得比较好:** _可以参考上面 Using CDCT_ 一节 **没有** `m_hAttribDC:` WTL的`CDCT` 没有`m_hAttribDC`成员。 **一些函数的参数稍有不同:** 例如:`MFC中的CDC::GetWindowExt()` 返回一个`CSize` ;而WTL版本的函数返回一个bool值,size的值通过输出参数返回。 ## 资源装载(Resource-Loading)函数 WTL 有几个全局函数对于装载各种资源很有帮助,在了解这些函数之前先要了解一下这个工具类:`_U_STRINGorID。` 在Win32平台上,有集中资源既可以用字符串标识(LPCTSTR),也可以用无符号整形数标识(UINT),那些使用资源的API都使用一个LPCTSTR类型的参数作为资源标识,如果你想传递一个UINT,你需要使用`MAKEINTRESOURCE`宏将其转换成LPCTSTR。`_U_STRINGorID`就是扮演一个转换资源标识类型的角色,它隐藏了两种类型的区别,函数调用者只需要直接传递`UINT` 或 `LPCTSTR` 就可以了,这个函数在必要的时候使用`CString` 装载字符串: ``` void somefunc ( _U_STRINGorID id ) { CString str ( id.m_lpstr ); // use str... } void func2() { // Call 1 - using a string literal somefunc ( _T("Willow Rosenberg") ); // Call 2 - using a string resource ID somefunc ( IDS_BUFFY_SUMMERS ); } ``` 这样使用之所以有效是因为CString的构造函数会检查LPCTSTR参数是不是一个字符串ID,如果是就从字符串资源表中装载这个字符串并赋值给`CString。` 在 VC 6版本中`_U_STRINGorID`由WTL提供,定义在`atlwinx.h`中,在 VC 7版本中,`_U_STRINGorID` 成为ATL的一部分,不过,对于使用没有区别,因为无论何种方式它都已经包含在你的ATL/WTL项目的头文件中了。 本节中介绍的函数从本地资源句柄装载资源,这个本地资源句柄保存在全局变量_Module(in VC 6)或者是全局变量`_AtlBaseModule`(in VC 7)中。使用其它模块的资源已经超出了本文的范畴,这里就不再介绍了。不过请记住,这些函数只能装载代码所在的EXE或DLL中的资源,它们仅仅是使用`_U_STRINGorID`带来的简化手段调用Win32的API而已。 ``` HACCEL AtlLoadAccelerators(_U_STRINGorID table) ``` 调用 `LoadAccelerators()。` ``` HMENU AtlLoadMenu(_U_STRINGorID menu) ``` 调用` LoadMenu()。` ``` HBITMAP AtlLoadBitmap(_U_STRINGorID bitmap) ``` 调用` LoadBitmap()。` ``` HCURSOR AtlLoadCursor(_U_STRINGorID cursor) ``` 调用 `LoadCursor()。` ``` HICON AtlLoadIcon(_U_STRINGorID icon) ``` 调用 `LoadIcon()`。注意,这个函数和`LoadIcon()` 一样只装载32x32 的图标。 ``` int AtlLoadString(UINT uID, LPTSTR lpBuffer, int nBufferMax) bool AtlLoadString(UINT uID, BSTR& bstrText) ``` 调用`LoadString()`。字符串可以返回在`TCHAR`中,也可以指定给一个`BSTR`,这要看你调用的是拿一个重载版本。注意,这个函数只接受UINT类型的资源ID,因为字符串表资源不能用字符串作为标识。 下面这一组函数封装了对`LoadImage()`的调用,使用一个额外的参数传递给`LoadImage()`。 ``` HBITMAP AtlLoadBitmapImage(_U_STRINGorID bitmap, UINT fuLoad = LR_DEFAULTCOLOR) ``` 调用`LoadImage()`,使用`IMAGE_BITMAP`类型,通过`fuLoad`传递标志字段。 ``` HCURSOR AtlLoadCursorImage( _U_STRINGorID cursor, UINT fuLoad = LR_DEFAULTCOLOR | LR_DEFAULTSIZE, int cxDesired = 0, int cyDesired = 0) ``` 调用`LoadImage()`,使用`IMAGE_CURSOR` 类型,使用`fuLoad` 标志。由于一个图标可能有几个不同大小的图标组成,所以另外两个参数用于指定所要装载图标的大小。 ``` HICON AtlLoadIconImage( _U_STRINGorID icon, UINT fuLoad = LR_DEFAULTCOLOR | LR_DEFAULTSIZE, int cxDesired = 0, int cyDesired = 0) ``` 调用`LoadImage()`,使用`IMAGE_ICON` 类型,通过`fuLoad` 传递标志字段。`cxDesired` `和cyDesired` 参数与`AtlLoadCursorImage()`函数作用一样。 下面这一组函数封装了一些操作系统定义资源的API(例如, 标准的手形鼠标指针)。默认情况下并不包含其中的一些资源ID (大多数是位图资源),你需要在stdafx.h 中定义`OEMRESOURCE` 标号才能使用它们。 ``` HBITMAP AtlLoadSysBitmap(LPCTSTR lpBitmapName) ``` 使用NULL资源句柄调用`LoadBitmap()` ,使用这个函数可以装载`LoadBitmap()`帮助文档中列举的一些`OBM_*`位图。 ``` HCURSOR AtlLoadSysCursor(LPCTSTR lpCursorName) ``` 使用NULL资源句柄调用`LoadCursor()`,使用这个函数可以装载`LoadCursor()`帮助文档中列举的`IDC_*`图标 ``` HICON AtlLoadSysIcon(LPCTSTR lpIconName) ``` 使用NULL资源句柄调用`LoadIcon()`,使用这个函数可以装载 `LoadIcon()` 帮助文档中列举的一些`IDI_*` 图标。需要注意的是这个函数和`LoadIcon()`一样只装载32x32 大小的图标。 ``` HBITMAP AtlLoadSysBitmapImage( WORD wBitmapID, UINT fuLoad = LR_DEFAULTCOLOR) ``` 使用NULL资源句柄调用`LoadImage()`,装载类型为`IMAGE_BITMAP` ,可以使用这个函数装载`AtlLoadSysBitmap()`能够装载的位图。 ``` HCURSOR AtlLoadSysCursorImage( _U_STRINGorID cursor, UINT fuLoad = LR_DEFAULTCOLOR | LR_DEFAULTSIZE, int cxDesired = 0, int cyDesired = 0) ``` 使用NULL资源句柄调用`LoadImage()`,装载类型为`IMAGE_CURSOR`,可以使用这个函数装载`AtlLoadSysCursor()`能够装载的图标。 ``` HICON AtlLoadSysIconImage( _U_STRINGorID icon, UINT fuLoad = LR_DEFAULTCOLOR | LR_DEFAULTSIZE, int cxDesired = 0, int cyDesired = 0) ``` 使用NULL资源句柄调用`LoadImage()`,装载类型为`IMAGE_ICON`,可以使用这个函数装载`AtlLoadSysIcon()`能够装载的图标,不过只能指定不同的大小,比如16x16。 最后这组函数提供了对`GetStockObject()` API的类型安全封装。 ``` HPEN AtlGetStockPen(int nPen) HBRUSH AtlGetStockBrush(int nBrush) HFONT AtlGetStockFont(int nFont) HPALETTE AtlGetStockPalette(int nPalette) ``` 这个函数首先将指定的参数转换成对GDI对象有效的类型 (比如`AtlGetStockPen()` 只接受`WHITE_PEN`和`BLACK_PEN`, 等等),然后直接调用`GetStockObject()`。 ## 使用通用对话框 WTL还提供了一些类用于简化对Win32 通用对话框地使用,这些类响应通用对话框发送的消息和回调函数,依次调用重载的消息处理函数。这种设计方式和属性页非常相似,你需要为每个属性页提供单独的通知消息响应函数(比如,`OnWizardNext()` 处理 `PSN_WIZNEXT`),它们在必要的时候由`CPropertyPageImpl`调用。 WTL 为每个通用对话框提供两个类,例如:选择文件夹对话框由`CFolderDialogImpl` 和 `CFolderDialog`两个类封装。如果你想改变它们的默认行为或为某个消息定制一个独特的响应函数,就需要从`CFolderDialogImpl`派生一个新类,在类中做相应的修改。如果默认的`CFolderDialogImpl`够用了,你就可以使用`CFolderDialog`。 通用对话框和对应的WTL类: | Common dialog | Corresponding Win32 API | Implementation class | Non-customizable class | | --- | --- | --- | --- | | File Open and File Save | `GetOpenFileName()`, `GetSaveFileName()` | `CFileDialogImpl` | `CFileDialog` | | Choose Folder | `SHBrowseForFolder()` | `CFolderDialogImpl` | `CFolderDialog` | | Choose Font | `ChooseFont()` | `CFontDialogImpl`, `CRichEditFontDialogImpl` | `CFontDialog`, `CRichEditFontDialog` | | Choose Color | `ChooseColor()` | `CColorDialogImpl` | `CColorDialog` | | Printing and Print Setup | `PrintDlg()` | `CPrintDialogImpl` | `CPrintDialog` | | Printing (Windows 2000 and later) | `PrintDlgEx()` | `CPrintDialogExImpl` | `CPrintDialogEx` | | Page Setup | `PageSetupDlg()` | `CPageSetupDialogImpl` | `CPageSetupDialog` | | Text find and replace | `FindText()`, `ReplaceText()` | `CFindReplaceDialogImpl` | `CFindReplaceDialog` | 介绍所有的类会使本文变成超级长文章,本文只介绍前两个最经常使用的类。 ### CFileDialog `CFileDialog`类和`CFileDialogImpl`类(译者注:一个是接口类,一个是实现类)用于显示文件打开和保存对话框,`CFileDialogImpl`类中最重要的两个成员是`m_ofn` 和`m_szFileName`。`m_ofn` 是一个`OPENFILENAME`结构,`和MFC一样,CFileDialogImpl` 使用一些有意义的默认值填充这个结构,如果有必要你可以直接操作这个成员修改其中的属性。`m_szFileName` 是一个`TCHAR` 数组,用来保存选择的文件名。 (`CFileDialogImpl` 只有一个字符串缓冲区保存文件名,如果要选择多个文件需要指定你自己的缓冲区)。 使用`CFileDialog` 的基本步骤: 1. 创建一个`CFileDialog`对象,通过构造函数传递一些初始数据。 2. 调用` DoModal()。` 3. 如果 `DoModal()` 返回`IDOK`,在`m_szFileName`中得到文件名。 下面是`CFileDialog类的构造函数`: ``` CFileDialog::CFileDialog ( BOOL bOpenFileDialog, LPCTSTR lpszDefExt = NULL, LPCTSTR lpszFileName = NULL, DWORD dwFlags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, LPCTSTR lpszFilter = NULL, HWND hWndParent = NULL ) ``` 创建打开文件对话框需要指定`bOpenFileDialog`为true(`CFileDialog`将调用`GetOpenFileName()` 显示对话框),创建文件保存对话框需要指定`bOpenFileDialog`为false(`CFileDialog` 调用`GetSaveFileName()`)。 其它参数对应着`m_ofn`结构中的成员,它们是可选的参数,因为你可以在调用`DoModal()`之前直接操作`m_ofn`修改这些值。 与MFC的`CFileDialog`有一点显著的不同,那就是`lpszFilter`参数必须是用null字符分隔的字符串列表(格式和`OPENFILENAME` 文档中说明的一样),而不是用“|”分隔的字符串列表。 下面的例子演示了使用带有filter的`CFileDialog`选择 Word 12 文件(`*.docx`)(译者注:传说中的office 2007): ``` CString sSelectedFile; CFileDialog fileDlg ( true, _T("docx"), NULL, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, _T("Word 12 Files\0*.docx\0All Files\0*.*\0") ); if ( IDOK == fileDlg.DoModal() ) sSelectedFile = fileDlg.m_szFileName; ``` `CFileDialog`类对本地化的支持不是很好,那是因为构造函数使用LPCTSTR类型的参数,不仅如此,filter字符串处理起来也很蹩脚。有两个解决方案,一是直接操作`m_ofn`,另一个是从`CFileDialogImpl`派生新类。这里我们采用第二种方式,派生一个新类,然后做如下修改: 1. 构造函数中的字符串参数使用`_U_STRINGorID` `代替LPCTSTR`。 2. 和MFC一样,filter 字符串改用“|”分隔,而不是null字符。 3. 对话框相对于父窗口自动居中。 我们开始编写一个新类,它的构造函数的参数和`CFileDialogImpl`的构造函数相似: ``` class CMyFileDialog : public CFileDialogImpl<CMyFileDialog> { public: // Construction CMyFileDialog ( BOOL bOpenFileDialog, _U_STRINGorID szDefExt = 0U, _U_STRINGorID szFileName = 0U, DWORD dwFlags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, _U_STRINGorID szFilter = 0U, HWND hwndParent = NULL ); protected: LPCTSTR PrepFilterString ( CString& sFilter ); CString m_sDefExt, m_sFileName, m_sFilter; }; ``` 构造函数初始化三个`CString` 成员,必要时可能从资源中装载字符串: ``` CMyFileDialog::CMyFileDialog ( BOOL bOpenFileDialog, _U_STRINGorID szDefExt, _U_STRINGorID szFileName, DWORD dwFlags, _U_STRINGorID szFilter, HWND hwndParent ) : CFileDialogImpl<CMyFileDialog>(bOpenFileDialog, NULL, NULL, dwFlags, NULL, hwndParent), m_sDefExt(szDefExt.m_lpstr), m_sFileName(szFileName.m_lpstr), m_sFilter(szFilter.m_lpstr) { } ``` 注意一点,这三个字符串在调用基类的构造函数的时候都是空的,这是因为基类的构造函数是在三个字符串初始化之前调用的,要设置`m_ofn`中的字符串数据,我们需要添加一些代码将`CFileDialogImpl` 构造函数中的初始化步骤重做一遍: ``` CMyFileDialog::CMyFileDialog(...) { m_ofn.lpstrDefExt = m_sDefExt; m_ofn.lpstrFilter = PrepFilterString ( m_sFilter ); // setup initial file name if ( !m_sFileName.IsEmpty() ) lstrcpyn ( m_szFileName, m_sFileName, _MAX_PATH ); } ``` `PrepFilterString()` 是一个辅助函数,将的filter字符串中的“|”分隔转换成null字符,结果就是将“|”分隔的filter字符串转换成`OPENFILENAME`所需要的格式。 ``` LPCTSTR CMyFileDialog::PrepFilterString(CString& sFilter) { LPTSTR psz = sFilter.GetBuffer(0); LPCTSTR pszRet = psz; while ( '\0' != *psz ) { if ( '|' == *psz ) *psz++ = '\0'; else psz = CharNext ( psz ); } return pszRet; } ``` 这些转换简化了字符串的处理。要实现窗口的自动居中显示,我们需要重载`OnInitDone()`,这需要添加消息映射(这样我们能够链接到基类的通知消息),下面是我们的`OnInitDone()`处理函数: ``` class CMyFileDialog : public CFileDialogImpl<CMyFileDialog> { public: // Construction CMyFileDialog(...); // Maps BEGIN_MSG_MAP(CMyFileDialog) CHAIN_MSG_MAP(CFileDialogImpl<CMyFileDialog>) END_MSG_MAP() // Overrides void OnInitDone ( LPOFNOTIFY lpon ) { GetFileDialogWindow().CenterWindow(lpon->lpOFN->hwndOwner); } protected: LPCTSTR PrepFilterString ( CString& sFilter ); CString m_sDefExt, m_sFileName, m_sFilter; }; ``` 关联到`CMyFileDialog` 对象的窗口实际上是文件打开对话框的一个子窗口,因为我们需要窗口队列的顶层窗口,所以调用`GetFileDialogWindow()`得到这个顶层窗口。 ### CFolderDialog `CFolderDialog`和`CFolderDialogImpl`类用来显示一个浏览文件夹的对话框,Windows的文件夹浏览对话框能够查看整个外壳名字空间(shell namespace)的任何位置,但是`CFolderDialog`,只支持浏览文件文件系统。`CFolderDialogImpl`中最重要的两个数据成员是`m_bi` 和 `m_szFolderPath`,`m_bi` 一个` BROWSEINFO` 类型的数据结构,它由`CFolderDialogImpl` 负责维护并作为参数传递给`SHBrowseForFolder()` API,必要时可以直接修改这个数据结构,`m_szFolderPath` 是一个`TCHAR` 类型的数组,它存放选中的文件夹全名。 使用`CFolderDialog`的步骤是: 1. 创建一个`CFolderDialog`对象,通过构造函数传递初始数据。 2. 调用 `DoModal()`。 3. 如果`DoModal()` 返回`IDOK`,就可以从`m_szFolderPath`获得文件夹名称。 下面是`CFolderDialog`的构造函数: ``` CFolderDialog::CFolderDialog ( HWND hWndParent = NULL, LPCTSTR lpstrTitle = NULL, UINT uFlags = BIF_RETURNONLYFSDIRS ) ``` `hWndParent` 是浏览对话框的拥有者窗口,可以通过构造函数在创建时指定拥有者窗口,也可以在调用`DoModal()` 时通过这个函数的参数指定拥有者窗口。`lpstrTitle` 是显示在浏览窗口中树控件上方的文字标签,`uFlags` 是一个标志,它决定了浏览对话框的行为。`uFlag`应该总是包括`BIF_RETURNONLYFSDIRS`属性,这样树控件就只显示文件系统的目录,有关这个标志的其它情况可以查阅关于`BROWSEINFO`数据结构的帮助文档,不过有一点需要了解,那就是并不是所有的标志属性都会产生好的作用,比如`BIF_BROWSEFORPRINTER`。不过与UI相关的一些标志工作的很好,比如`BIF_USENEWUI`。还有一点就是构造函数中的`lpstrTitle`参数在使用时会有点小问题。 下面是使用`CFolderDialog`选择目录的例子: ``` CString sSelectedDir; CFolderDialog fldDlg ( NULL, _T("Select a dir"), BIF_RETURNONLYFSDIRS|BIF_NEWDIALOGSTYLE ); if ( IDOK == fldDlg.DoModal() ) sSelectedDir = fldDlg.m_szFolderPath; ``` 现面演示一下如何使用定制的`CFolderDialog`,我们从`CFolderDialogImpl`类派生一个新类并设置初始选择,由于这个对话框的回调不使用Windows消息,所以新类也不需要消息映射链,只需重载`OnInitialized()`函数即可,这个函数在基类接收到`BFFM_INITIALIZED` 通知消息时被调用,`OnInitialized()`调用`CFolderDialogImpl::SetSelection()` 改变对话框的初始选择。 ``` class CMyFolderDialog : public CFolderDialogImpl<CMyFolderDialog> { public: // Construction CMyFolderDialog ( HWND hWndParent = NULL, _U_STRINGorID szTitle = 0U, UINT uFlags = BIF_RETURNONLYFSDIRS ) : CFolderDialogImpl<CMyFolderDialog>(hWndParent, NULL, uFlags), m_sTitle(szTitle.m_lpstr) { m_bi.lpszTitle = m_sTitle; } // Overrides void OnInitialized() { // Set the initial selection to the Windows dir. TCHAR szWinDir[MAX_PATH]; GetWindowsDirectā????用????????ory ( szWinDir, MAX_PATH ); SetSelection ( szWinDir ); } protected: CString m_sTitle; }; ``` ## 其它有用的类和全局函数 ### 对结构的封装 和MFC一样,WTL 也对`SIZE`、`POINT`和`RECT`数据结构进行了封装,分别是`CSize`、`CPoint`和`CRect`类。 ### 处理双类型参数的类 就像前面提到的那样,你可以使用`_U_STRINGorID` 去自适应那些参数是数字或字符串资源ID的函数,WTL中还有两个类和这个类的作用类似: * `_U_MENUorID`: 这个类型支持`UINT` 或 `HMENU`,通常用在`CreateWindow()` 的封装中函数中,`hMenu` 参数在某些情况下是菜单句柄,但是在创建子窗口时它又是一个窗口ID,`_U_MENUorID` 用来消除(隐藏)这些差异,`_U_MENUorID` 有一个`m_hMenu`成员,用来向`CreateWindow()` 或 `CreateWindowEx()`传递hMenu参数。 * `_U_RECT`: 这个类可以从`LPRECT` 或`RECT&`构建,可以将`RECT` 数据结构,`RECT`指针或象`CRect`那样的封装类转换成很对函数需要的`RECT`类型参数。 和`_U_STRINGorID`一样,`_U_MENUorID` 和`_U_RECT` 也已经随着其它头文件包含在你的工程中了。 ### 其它工具类 #### CString WTL的`CString`和MFC的`CString`类似,所以这里就不用详细介绍了, 不过,WTL的`CString` 还多了了一些额外的特性,这些特性在你使用`_ATL_MIN_CRT`方式编译代码的时候就显得十分有用,比如,`_cstrchr()` 和` _cstrstr()`函数。当不使用`_ATL_MIN_CRT`方式编译时它们会被相应的CRT函数取代,不会产生额外的代码。(译者注:使用`_ATL_MIN_CRT`方式编译是为了减少对CRT库的依赖,从而产生较小的可执行文件,不过这样就不能使用很多CRT的标准函数,比如`strstr`、`strchr`等等,在这种情况下WTL的`CString`会使用自己的函数代替,从而保证`CString`能够正常工作,当不使用`_ATL_MIN_CRT`方式时,`CString`会直接调用CRT库函数) #### CFindFile `CFindFile` 封装了`FindFirstFile()`和`FindNextFile()` APIs,它比MFC的`CFileFind`还要容易使用一些,使用方式可以参考下面的模式: ``` CFindFile finder; CString sPattern = _T("C:\\windows\\*.exe"); if ( finder.FindFirstFile ( sPattern ) ) { do { // act on the file that was found } while ( finder.FindNextFile() ); } finder.Close(); ``` 如果`FindFirstFile()` 返回true,就表示至少有一个文件匹配查找模式,在循环内,你可以访问`CFindFile` 的公有成员`m_fd`,这是一个`WIN32_FIND_DATA` 数据结构,包含了这个文件的信息,循环可以一直继续直到`FindNextFile()` 返回false,这表示你已经把所有的文件遍历了一遍。 为了使用方便,`CFindFile` 还提供了一些操作`m_fd`的函数,这些函数的返回值只在成功调用了`FindFirstFile`或`FindNextFile()`之后才有意义。 ``` ULONGLONG GetFileSize() ``` 返回文件的大小,数据类型时64位无符号整形数。 ``` BOOL GetFileName(LPTSTR lpstrFileName, int cchLength) CString GetFileName() ``` 得到查找到文件的名字和扩展名(`从m_fd.cFileName`复制数据)。 ``` BOOL GetFilePath(LPTSTR lpstrFilePath, int cchLength) CString GetFilePath() ``` 返回查找到文件的全路径。 ``` BOOL GetFileTitle(LPTSTR lpstrFileTitle, int cchLength) CString GetFileTitle() ``` 返回文件的标题 (就是没有扩展名)。 ``` BOOL GetFileURL(LPTSTR lpstrFileURL, int cchLength) CString GetFileURL() ``` 创建一个`file://` URL,包含文件的全路径。(译者注:比如“file://d:\doc\sss.doc”) ``` BOOL GetRoot(LPTSTR lpstrRoot, int cchLength) CString GetRoot() ``` 得到文件所在的目录。 ``` BOOL GetLastWriteTime(FILETIME* pTimeStamp) BOOL GetLastAccessTime(FILETIME* pTimeStamp) BOOL GetCreationTime(FILETIME* pTimeStamp) ``` 这些函数从`m_fd`的数据成员`ftLastWriteTime`、`ftLastAccessTime`和`ftCreationTime` 复制数据。 `CFindFile` 还有一些辅助函数用于检查文件的属性。 ``` BOOL IsDots() ``` 如果文件是"`.`" 或 "`..`" 目录就返回true。 ``` BOOL MatchesMask(DWORD dwMask) ``` 将文件的属性和`dwMask` (通常是一些`FILE_ATTRIBUTE_*` 属性的组合)比较,看看查找到的文件是是否有指定的属性如果文件属性包含dwMask指定的位掩码,就返回true。 ``` BOOL IsReadOnly() BOOL IsDirectory() BOOL IsCompressed() BOOL IsSystem() BOOL IsHidden() BOOL IsTemporary() BOOL IsNormal() BOOL IsArchived() ``` 这些函数是`MatchesMask()` 函数的更直观的替代者,它们通常只是测试属性中的某一个,比如,`IsReadOnly()` 就是调用`MatchesMask(FILE_ATTRIBUTE_READONLY)`。 ### 全局函数 WTL 还有一些很有用的全局函数,比如检查 DLL 版本或者显示一个消息框窗口。 ``` bool AtlIsOldWindows() ``` 判断Windows的版本是否太老,如果是Windows 95、 98、 NT 3 或 NT 4这样的系统就会返回true。 ``` HFONT AtlGetDefaultGuiFont() ``` 返回值和调用`GetStockObject(DEFAULT_GUI_FONT)`的返回值相同,在英文版的 Windows 2000 或更新的版本 (也包括一些使用拉丁字母的单字节语言)中,这个字库的名字(face name)是“MS Shell Dlg”。这(种字体)对于对话框效果很好,但是如果你要在界面上创建自己的字体,这就不是一个很好的选择。MS Shell Dlg 就是 MS Sans Serif的别名, 而不是使用新字体Tahoma。 要避免使用MS Sans Serif,你可以通过消息框得到字体: ``` NONCLIENTMETRICS ncm = { sizeof(NONCLIENTMETRICS) }; CFont font; if ( SystemParametersInfo ( SPI_GETNONCLIENTMETRICS, 0, &ncm, false ) ) font.CreateFontIndirect ( &ncm.lfMessageFont ); ``` 另一种方法是检查`AtlGetDefaultGuiFont()`返回的字体名称,如果是“MS Shell Dlg”就将其改成“MS Shell Dlg 2”,它将使用新字体 Tahoma。 ``` HFONT AtlCreateBoldFont(HFONT hFont = NULL) ``` 创建指定字体的加粗版本,如果`hFont` 是 NULL,`AtlCreateBoldFont()` 就创建一个通过`AtlGetDefaultGuiFont()`得到的字体的加粗版本。 ``` BOOL AtlInitCommonControls(DWORD dwFlags) ``` 这是对`InitCommonControlsEx()` API的封装,使用一些指定的标志初始化`INITCOMMONCONTROLSEX` 结构,然后调用`InitCommonControlsEx()`。 ``` HRESULT AtlGetDllVersion(HINSTANCE hInstDLL, DLLVERSIONINFO* pDllVersionInfo) HRESULT AtlGetDllVersion(LPCTSTR lpstrDllName, DLLVERSIONINFO* pDllVersionInfo) ``` 这两个函数在指定的模块种查找名为`DllGetVersion()`的导出函数,如果函数存在就调用这个函数,如果调用成功就返回一个`DLLVERSIONINFO`结构的版本信息。 ``` HRESULT AtlGetCommCtrlVersion(LPDWORD pdwMajor, LPDWORD pdwMinor) ``` 返回comctl32.dll的主版本号和次版本号。 ``` HRESULT AtlGetShellVersion(LPDWORD pdwMajor, LPDWORD pdwMinor) ``` 返回shell32.dll的主版本号和次版本号。 ``` bool AtlCompactPath(LPTSTR lpstrOut, LPCTSTR lpstrIn, int cchLen) ``` 将一个文件名截断,从而使其长度小于`cchLen`,在结尾添加省略号,它和`shlwapi.dll`中的`PathCompactPath()` 和 `PathSetDlgItemPath()` 功能相似。 ``` int AtlMessageBox(HWND hWndOwner, _U_STRINGorID message, _U_STRINGorID title = NULL, UINT uType = MB_OK | MB_ICONINFORMATION) ``` 和`MessageBox()`一样,显示一个消息框,但是使用了`_U_STRINGorID` 参数,这样你就可以传递字符串资源ID作为参数,`AtlMessageBox()` 会自动装载字符串资源。 ### 宏 在WTL 的头文件中你会看到各种各样的预处理宏,大多数的宏都可以在编译选项中设置,从而改变WTL代码的某些行为。 以下几个宏与编译设置紧密相关,在WTL的代码中到处都有它们的身影: `_WTL_VER` 对于 WTL 7.1 被定义为 0x0710。 `_ATL_MIN_CRT` 如果这个宏被定义了,ATL将不链接C标准库,由于很多WTL的类(尤其是CString)都要使用C标准库函数,很多特殊的代码被编译进来以代替CRT函数。 `_ATL_VER` 对于VC6,这个版本是`0x0300`,对于VC7,这个版本是`0x0700` ,对于VC8,这个版本是 0x0800。 `_WIN32_WCE` 是否编译的是 Windows CE 二进制映象,一些WTL代码因为Windows CE不支持相应的特性而不可用。 下面的宏默认是不定义的,要使用它们需要在`stdafx.h`文件中所有`#include`语句之前定义它们。 `_ATL_NO_OLD_NAMES` 这个宏只在维护WTL 3的代码时起作用,它增加了很多编译检测用于识别两个老的类名和函数名:`CUpdateUIObject` 已经改名为 `CIdleHandler`,`DoUpdate()`也改名为`OnIdle()`。 `_ATL_USE_CSTRING_FLOAT` 定义这个标号可以使`CString`支持浮点运算,它不能和`_ATL_MIN_CRT`一起使用,如果想在 `CString::Format()`函数中使用%I64格式,就必须定义这个选项。如果定义了`_ATL_USE_CSTRING_FLOAT` 标号,`CString::Format()` 将调用`_vstprintf()`,这个函数能够识别`%I64` 格式。 `_ATL_USE_DDX_FLOAT` 定义这个标号可以使 DDX 代码支持浮点运算,它不能和`_ATL_MIN_CRT`同时使用。 `_ATL_NO_MSIMG` 定义这个标号将使编译器忽略`#pragma comment(lib, "msimg32")` 代码行,也就是禁止`CDCT` 使用 msimg32 函数: `AlphaBlend()`、`TransparentBlt()`和`GradientFill()`。 `_ATL_NO_OPENGL` 定义这个标号编译器将忽略`#pragma comment(lib, "opengl32")` 代码行,也就是禁止`CDCT` 使用 OpenGL。 `_WTL_FORWARD_DECLARE_CSTRING` 已经废弃,使用`_WTL_USE_CSTRING` 代替。 `_WTL_USE_CSTRING` 定义这个标号将向前声明`CString`类,这样,那些在`atlmisc.h` 文件之前包含的头文件也可以使用`CString`。 `_WTL_NO_CSTRING` 定义这个标号将不能使用`WTL::CString`。 `_WTL_NO_AUTOMATIC_NAMESPACE` 定义这个标号将阻止直接使用WTL命名空间(`namespace`)。 `_WTL_NO_AUTO_THEME` 定义这个标号阻止`CMDICommandBarCtrlImpl` 使用 XP 主题。 `_WTL_NEW_PAGE_NOTIFY_HANDLERS` 定义这个标号可以在`CPropertyPage`中使用较新的`PSN_*` 通知消息响应函数,因为老的WTL 3的消息响应函数已经废弃掉了,这个标号应该总是被定义,除非你是在维护老的WTL3 代码。 `_WTL_NO_WTYPES` 定义这个标号将禁止使用WTL封装的`CSize`、`CPoint`和`CRect`类。 `_WTL_NO_THEME_DELAYLOAD` 在VC6中编译代码时,定义这个标号将阻止_uxtheme.dll_ 被自动标记为延时加载。 注意: 如果`_WTL_USE_CSTRING` `和_WTL_NO_CSTRING` 同时定义,产生的结果就只能在`atlmisc.h` 文件包含之后使用`CString`。 ## 例子工程 本文的演示工程是一个下载工具,这个名为Kibbles的下载工具演示了几个本文介绍的类。这个下载工具使用了BITS([background intelligent transfer service](http://msdn.microsoft.com/library/en-us/bits/bits/bits_start_page.asp))组件,Windows 2000及其以后的操作系统都支持这个组件,也就是说这个程序只能运行在基于NT技术的操作系统上,所以我就将其创建成了Unicode工程。 这个程序有一个视图窗口,这个视图窗口用来显示下载过程,它使用了很多GDI函数,也包括专门画饼图的Pie()函数。第一次运行时,程序的初始界面是这样的: 你可以从浏览器中拖一个链接到这个窗口中,程序会创建一个新的BITS并将链接指定的目标下载到“我的文档”文件夹。当然也可以单击工具栏上第三个按钮直接添加一个URL,工具栏上的第四个按钮则允许你修改默认的下载文件存放位置。 当一个下载任务正在进行,Kibbles会显示一些下载任务的细节,下载的过程显示如下: ![](https://box.kancloud.cn/2016-01-13_56962fbe63987.png) 工具栏上的前两个按钮用来修改过程显示的颜色,第一个按钮会打开一个选项对话框,在选项对话框中可以设置过程饼图中各部分的颜色: ![](https://box.kancloud.cn/2016-01-13_56962fbe72839.png) 对话框中使用了Tim Smith的文章“[Color Picker for WTL with XP themes](http://www.codeproject.com/wtl/wtlcolorbutton.asp)”中介绍的一个很棒的按钮类,可以查看Kibbles工程中的`CChooseColorsDlg` 类的代码了解这个按钮类是如何工作的。_“Text color”_ 按钮是一个普通的按钮,它的响应函数`OnChooseTextColor()`演示了如何使用WTL的`CColorDialog`类。第二个工具栏按钮的功能是使用随即颜色显示下载过程。 第五个工具栏按钮用来设置背景图片,Kibbles使用这个图片在饼图上显示下载过程。默认的图片程序资源中包含的一个图片,你也可以选择任何BMP位图文件作为背景图片: ![](https://box.kancloud.cn/2016-01-13_56962fbe8bb86.png) `CMainFrame::OnToolbarDropdown()` 的代码响应按钮的press事件并显示一个弹出式菜单,这个函数还使用了`CFindFile` 类遍历“我的文档”文件夹。关于各种GDI函数的用法可以查看`CKibblesView::OnPaint()`函数的代码。 关于工具栏有一点需要特别注意:这个工具栏使用了256色的位图,不过VC的工具栏编辑器只支持16色位图,如果你使用工具栏编辑器修改过工具栏位图,你的工具栏位图就会被转换成16色。我的建议是,在另一个目录中保存这个高彩色的位图,使用图像编辑工具直接编辑这个图片,然后在“res”目录中另存一个256色的版本。 ## 版权和许可协议 这篇文章受版权保护, (c)2006 by Michael Dunn。我知道不能阻止人们通过网络拷贝这些文章,但是我必须要说的是,如果你有兴趣翻译本系列文章,请一定让我知道(汗....,还好翻译第一篇之前给Michael发了一封邮件),我不会拒绝你的翻译请求,我只是想知道我的文章被翻译了几个版本,这样我可以在这里给出相应版本的链接。 有两个文件除外,就是_ColorButton.cpp_ 和 _ColorButton.h_,这篇文章附带的演示代码是向所有人公开的,这样每个人都可以从代码中受益。 (我不让文章也向所有人(版权)公开是因为这样能够提高我自己和CodeProject网站的知名度(%……¥%¥#%¥#晕)。) 如果你的程序中使用了我的演示代码,最好能给我发个Email,当然这不是强制要求,只是为了满足我的一点好奇心,我想知道是否有人从我的代码中受益。 _文件ColorButton.cpp_ 和 _ColorButton.h_ 来自Tim Smith的文章“[Color Picker for WTL with XP themes](http://www.codeproject.com/wtl/wtlcolorbutton.asp)”,它们不包含在上面的许可声明中,它们的许可声明包含在文件的注释部分。 ## 修订历史 2006年2月8日,第一次发布。