特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
内容
- 简介
- GDI 封装类
- 封装类里的公用函数
- 使用 CDCT
- 与 MFC 封装类的差异
- 资源加载函数
- 使用公用对话框
- CFileDialog
- CFolderDialog
- 其他有用的类和全局函数
- Struct 的封装
- 处理双类型参数的类
- 其他工具类
- 全局函数
- 宏
- 示例工程
- 版权和许可
- 修订历史
简介
WTL 里包含了好多封装类和工具类,而直到现在也还没有在本系列里进行过全面的介绍,比如说 CString 和 CDC。WTL 具有一个封装 GDI 对象的良好体系,一些用以加载资源的便利函数,以及更便于使用某些 Win32 公用对话框的类。在此第九部分里,我将介绍一些使用最广泛的类。
本文讨论了四类特性:
- GDI 封装类
- 资源加载函数
- 使用打开文件公用对话框以及文件夹选择公用对话框
- 其他有用的类以及全局函数
GDI 封装类
相较于 MFC,WTL 为其 GDI 封装类使用了一种相当不同的方法。WTL 的方法是,为每种类型的 GDI 对象准备一个模板类,每个模板都有一个名为 t_bManaged 的 bool 参数。此参数控制着该类的实例是否“管理”着(或者说是拥有)被封装的 GDI 对象。如果 t_bManaged 为 false,则 C++ 对象不会管理 GDI 对象的生命期,C++ 对象仅是围绕 GDI 对象句柄的一个简单封装层。如果 t_bManaged 为 true,则有两件事情会改变:
- 析构函数会对所封装的非空句柄调用 DeleteObject()。
- Attach() 在将 C++ 对象关联到新的句柄之前,也会对所封装的非空句柄调用 DeleteObject()。
此设计与 ATL 窗口类的设计是一致的, CWindow 是对 HWND 的一个简单的封装层,而 CWindowImpl 管理了一个窗口的生命期。
GDI 封装类定义于 atlgdi.h 中,不过 CMenuT 是个例外,它定义于 atluser.h 中(译者注:此处作者的说法欠妥,菜单在 Windows 里原本就是属于 USER32 的,而不是 GDI32)。你自己不必包含这些头文件,因为 atlapp.h 总会为你包含它们。每个类还有一个易于记忆的 typedef 名字:
封装的 GDI 对象 | 模板类 | 管理对象的 typedef 名 | 普通封装的 typedef 名 |
---|---|---|---|
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 传递对象指针相比较,我更喜欢这种方法。你永远也不必担心收到了一个 NULL 指针(封装的句柄可能会是 NULL,但那是另一回事),也不会有这种特殊的情况,你有一个临时对象,但你却不能把它保存下来然后对它进行不止一次的函数调用。创建这些类的实例代价 也是很低的,因为它们只有一个成员变量,即所封装的句柄。就像 CWindow 一样,在线程之间传递封装类的对象也不会有问题,因为 WTL 并没有像 MFC 那样的线程相关的映射。
另外还有几个设备上下文的封装类,用于特定的绘制场景:
- CClientDC:封装了对 GetDC() 和 ReleaseDC() 的调用,用于在窗口的客户区内绘制
- CWindowDC:封装了对 GetWindowDC() 和 ReleaseDC() 的调用,用于在窗口内的任意地方绘制
- CPaintDC:封装了对 BeginPaint() 和 EndPaint() 的调用,用于 WM_PAINT 处理器中
这些类中的任一个都是在构造函数中接受一个 HWND,而行为与其在 MFC 中的同名类相似。所有这三个类都派生于 CDC,因此它们都管理着自己的设备上下文。
封装类里的公用函数
GDI 封装类都遵循相同的设计。简明起见,在这儿我只介绍 CBitmapT 里的方法,但其它类工作起来都很相似。
- 封装的 GDI 对象句柄
- 每个类都持有一个公用成员变量,其中保留了此 C++ 对象关联着的 GDI 对象的句柄。 CBitmapT 则具有一个名为 m_hBitmap 的 HBITMAP 成员。
- 构造函数
- 构造函数有一个参数,为一个 HBITMAP,其缺省值为 NULL。 m_hBitmap 被初始化为此参数的值。
- 析构函数
- 如果 t_bManaged 为 true,而且 m_hBitmap 不是 NULL,则析构函数会调用 DeleteObject() 以销毁位图。
- Attach() 和 operator =
- 这两个方法都接受一个 HBITMAP 句柄。如果 t_bManaged 为 true,而且 m_hBitmap 不是 NULL,这些方法会先调用 DeleteObject() 以销毁 CBitmapT 对象管理着的位图,然后将 m_hBitmap 设置为作为参数传入的 HBITMAP。
- Detach()
- Detach() 会将 m_hBitmap 设置为 NULL,然后再返回原来在 m_hBitmap 中的值。当 Detach() 返回后, CBitmapT 对象就不再与任何 GDI 有关联了。
- 创建 GDI 对象的方法
- CBitmapT 对 Win32 中创建位图的 API 也有封装: LoadBitmap()、 LoadMappedBitmap()、 CreateBitmap()、 CreateBitmapIndirect()、 CreateCompatibleBitmap()、 CreateDiscardableBitmap()、 CreateDIBitmap() 以及 CreateDIBSection()。如果 m_hBitmap 不是 NULL 时,这些方法都会产生断言。如果要为另一个 GDI 位图重用本 CBitmapT 对象,就要先调用 DeleteObject() 或者 Detach()。
- DeleteObject()
- DeleteObject() 销毁 GDI 位图对象并将 m_hBitmap 置为 NULL。这一方法仅当 m_hBitmap 不为 NULL 时才应该调用,否则会产生断言。
- IsNull()
- IsNull() 在 m_hBitmap 为 NULL 时返回 true,否则返回 false。这一方法用来测试 CBitmapT 对象当前是否关联着一个 GDI 位图。
- operator HBITMAP
- 这个转换器会返回
m_hBitmap,这就使你可以把
CBitmapT 对象传递给一个接收
HBITMAP 句柄的函数或者 Win32 API。这个转换器在以下情况下也会被调用,或者是一个
CBitmapT 在布尔值的上下文环境下被求值,或者是对
IsNull() 的逻辑取反求值。因而,这两个 if 语句是等效的:
1234CBitmapHandle bmp = /* some HBITMAP value */;if ( !bmp.IsNull() ) { do something... }if ( bmp ) { do something more... } - GetObject() 的封装
-
CBitmapT 具有一个对于 Win32 API
GetObject()的类型安全的封装:
GetBitmap()。有两个重载形态:一个是接收
LOGBITMAP* 参数而直接调用
GetObject(),另一个则接收
LOGBITMAP& 参数并返回一个表示成功与否的
bool 值。后者更为易用些。例如:
123456CBitmapHandle bmp2 = /* some HBITMAP value */;LOGBITMAP logbmp = {0};bool bSuccess;if ( bmp2 )bSuccess = bmp2.GetLogBitmap ( logbmp ); - 操作 GDI 对象的 API 的封装
- CBitmapT 对接收 HBITMAP 参数的 Win32 API 也进行了封装: GetBitmapBits()、 SetBitmapBits()、 GetBitmapDimension()、 SetBitmapDimension()、 GetDIBits() 以及 SetDIBits()。这些方法在 m_hBitmap 为 NULL 时都会产生断言。
- 其它辅助方法
- CBitmapT 还有两个作用于 m_hBitmap 上的有用的方法: LoadOEMBitmap() 和 GetSize()。
使用 CDCT
CDCT 与其它类有一点不同,因此我要单独介绍其差别。
方法上的差别
销毁一个 DC 的方法叫做 DeleteDC() 而不是 DeleteObject()。
将对象选入 DC
MFC 的 CDC 在把对象选入到其中时有一个容易导致错误的地方。MFC 的 CDC 有若干个重载的 SelectObject() 函数,每个都是接收一个指向不同类型的 GDI 封装类的指针( CPen*, CBitmap*,等等)作为参数。如果你把一个 C++ 对象而不是指向该对象的指针传递给 SelectObject(),代码最终会调用一个未文档化的,接收一个 HGDIOBJ 句柄为参数的重载,这样就会导致问题。
WTL 的 CDCT 使用了一种更好的方法,它有好几个选择函数,每一个都只作于特定类型的 GDI 对象:
1 2 3 4 5 6 |
HPEN SelectPen(HPEN hPen) HBRUSH SelectBrush(HBRUSH hBrush) HFONT SelectFont(HFONT hFont) HBITMAP SelectBitmap(HBITMAP hBitmap) int SelectRgn(HRGN hRgn) HPALETTE SelectPalette(HPALETTE hPalette, BOOL bForceBackground) |
在调试模式的构建中,每个方法都会断言 m_hDC 为非 NULL 值,而且参数是恰当类型的 GDI 对象的句柄。然后再调用 SelectObject() API,并将其返回值转型为合适的类型。
还存在着几个辅助函数,它们先用给定的常量调用 GetStockObject(),然后再把对象选入到 DC:
1 2 3 4 |
HPEN SelectStockPen(int nPen) HBRUSH SelectStockBrush(int nBrush) HFONT SelectStockFont(int nFont) HPALETTE SelectStockPalette(int nPalette, BOOL bForceBackground) |
与 MFC 封装类的区别
较少的构造函数:WTL 的封装类缺少可以创建新的 GDI 对象的构造函数。比如,MFC 的 CBrush 有一个构造函数可以创建一个实心画刷或者模式(patterne)画刷。而使用 WTL 的类,你必须使用某个方法来创建该 GDI 对象。
用更好的方法把对象选入 DC:参看上面使用 CDCT 节
没有 <b>m_hAttribDC</b>:WTL 的 CDCT 没有 m_hAttribDC 成员
一些方法里有少许的参数差异:例如, CDC::GetWindowExt() 在 MFC 里返回一个 CSize 对象,而在 WTL 里返回一个 bool,而大小通过输出参数来返回。
资源加载函数
WTL 里的若干个全局函数是极具帮助的加载不同类型的资源的捷径。在接触这些函数之前,我们需要了解一个工具类: _U_STRINGorID。
在 Win32 中,大多数资源可以用一个字符串( LPCTSTR)或者一个无符号整数( UINT)来标识。接收资源标识符的 API 需要接收一个 LPCTSTR 参数,如果要传递一个 UINT 的话,你需要使用 MAKEINTRESOURCE 宏来将之转换为一个 LPCTSTR。 _U_STRINGorID,当作为资源标识符参数类型来使用的时候,会隐藏这一不同,因而调用者就可以直接传递 UINT 或者 LPCTSTR。如果需要的话,函数可以使用一个 CString 来加载字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 全局变量(VC 6)或者 _AtlBaseModule (VC 7)中的资源实例句柄加载资源。使用其他模块的资源超出了本文的范围,因此我在这儿不会提及。只需要记住,缺省情况下,这些函数总是在代码正运行于其中的 EXE 或者 DLL 中寻找。这些函数并没有比调用 API 多做什么,它们的作用在于由 _U_STRINGorID 所提供的资源标识符的处理简化上。
1 HACCEL AtlLoadAccelerators(_U_STRINGorID table)调用流向 LoadAccelerators()。
1 HMENU AtlLoadMenu(_U_STRINGorID menu)调用流向 LoadMenu()。
1 HBITMAP AtlLoadBitmap(_U_STRINGorID bitmap)调用流向 LoadBitmap()。
1 HCURSOR AtlLoadCursor(_U_STRINGorID cursor)调用流向 LoadCursor()。
1 HICON AtlLoadIcon(_U_STRINGorID icon)调用流向 LoadIcon()。注意一下这个函数,和 LoadIcon() 一样,仅可以加载 32×32 图标。
12 int AtlLoadString(UINT uID, LPTSTR lpBuffer, int nBufferMax)bool AtlLoadString(UINT uID, BSTR& bstrText)调用流向 LoadString()。字符串可以返回至一个 TCHAR 缓冲区,或者是赋与一个 BSTR,取决于你使用哪一个重载。注意这两个函数仅接收 UINT 作为其资源 ID,因为字符串表项不能有字符串标识符。
下面这组函数封装了对 LoadImage() 的调用,而且还接受传递给 LoadImage() 的附加参数。
12 HBITMAP AtlLoadBitmapImage(_U_STRINGorID bitmap, UINT fuLoad = LR_DEFAULTCOLOR)使用 IMAGE_BITMAP 类型调用 LoadImage(),并将 fuLoad 标志传入。
1234 HCURSOR AtlLoadCursorImage(_U_STRINGorID cursor,UINT fuLoad = LR_DEFAULTCOLOR | LR_DEFAULTSIZE,int cxDesired = 0, int cyDesired = 0)使用 IMAGE_CURSOR 类型调用 LoadImage(),并将 fuLoad 标志传入。由于一个光标资源可以包含若干个不同尺寸的光标,因而你可以把尺寸传递给 cxDesired 和 cyDesired 参数以加载一个特定大小的光标。
1234 HICON AtlLoadIconImage(_U_STRINGorID icon,UINT fuLoad = LR_DEFAULTCOLOR | LR_DEFAULTSIZE,int cxDesired = 0, int cyDesired = 0)使用 IMAGE_ICON 类型调用 LoadImage(),并将 fuLoad 标志传入。 cxDesired 和 cyDesired 参数的用法同 AtlLoadCursorImage()。
下面这一组函数封装了加载系统定义的资源(例如,标准的手状光标)的调用。其中的一些资源 ID(主要是位图的那些)缺省是不被包含的,需要在你的 stdafx.h 文件中 #define OEMRESOURCE 符号来引用它们。
1 HBITMAP AtlLoadSysBitmap(LPCTSTR lpBitmapName)使用 NULL 资源句柄调用 LoadBitmap()。使用此函数可以加载任何在 LoadBitmap() 的文档中列出的 OBM_* 位图。
1 HCURSOR AtlLoadSysCursor(LPCTSTR lpCursorName)使用 NULL 资源句柄调用 LoadCursor()。使用此函数可以加载任何在 LoadCursor() 的文档中列出的 IDC_* 光标。
1 HICON AtlLoadSysIcon(LPCTSTR lpIconName)使用 NULL 资源句柄调用 LoadIcon()。使用此函数可以加载任何在 LoadIcon() 的文档中列出的 IDI_* 图标。不过要注意此函数 —— 就像 LoadIcon() —— 只能加载 32×32 的图标。
12 HBITMAP AtlLoadSysBitmapImage(WORD wBitmapID, UINT fuLoad = LR_DEFAULTCOLOR)使用 NULL 资源句柄以及 IMAGE_BITMAP 类型调用 LoadImage()。你可以使用此函数来加载与 AtlLoadSysBitmap() 相同的位图。
1234 HCURSOR AtlLoadSysCursorImage(_U_STRINGorID cursor,UINT fuLoad = LR_DEFAULTCOLOR | LR_DEFAULTSIZE,int cxDesired = 0, int cyDesired = 0)使用 NULL 资源句柄以及 IMAGE_CURSOR 类型调用 LoadImage()。你可以使用此函数来加载与 AtlLoadSysCursor() 相同的光标。
1234 HICON AtlLoadSysIconImage(_U_STRINGorID icon,UINT fuLoad = LR_DEFAULTCOLOR | LR_DEFAULTSIZE,int cxDesired = 0, int cyDesired = 0)使用 NULL 资源句柄以及 IMAGE_ICON 类型调用 LoadImage()。你可以使用此函数来加载与 AtlLoadSysIcon() 相同的图标,不过可以指定一个不同的尺寸,例如 16×16。
最后的这组函数是对 GetStockObject() API 的类型安全的封装。
1234 HPEN AtlGetStockPen(int nPen)HBRUSH AtlGetStockBrush(int nBrush)HFONT AtlGetStockFont(int nFont)HPALETTE AtlGetStockPalette(int nPalette)
每个函数都会检查你是不是传入了恰当的值(比方说, AtlGetStockPen() 只接受 WHITE_PEN, BLACK_PEN 等值),然后再调用 GetStockObject()。
使用公用对话框
WTL 还有些类,可以使 Win32 的公用对话框使用起来更容易些。每个类都会处理公用对话框发送的消息以及回调,并依次调用那些可覆盖的函数。这和用于属性表的设计是一样的,只不过在那儿你是为各个被 CPropertyPageImpl 在必要时调用的属性表通知编写处理器,例如 OnWizardNext(),用来处理 PSN_WIZNEXT。
WTL 为每种公用对话框都包含了两个类,例如,选择文件夹对话框由 CFolderDialogImpl 和 CFolderDialog 封装。如果需要改变缺省行为,或者是要为某个消息编写处理器,你可以从 CFolderDialogImpl 派生一个新类并在其中作改动。如果 CFolderDialogImpl 的缺省行为已经足够了,则你可以使用 CFolderDialog。
公用对话框及其相应的 WTL 类有:
公用对话框 | 相应的 Win32 API | 实现类 | 不可定制类 |
---|---|---|---|
文件打开和文件保存 |
GetOpenFileName(), GetSaveFileName() |
CFileDialogImpl | CFileDialog |
选择文件夹 | SHBrowseForFolder() | CFolderDialogImpl | CFolderDialog |
选择字体 | ChooseFont() |
CFontDialogImpl, CRichEditFontDialogImpl |
CFontDialog, CRichEditFontDialog |
选择颜色 | ChooseColor() | CColorDialogImpl | CColorDialog |
打印及打印设置 | PrintDlg() | CPrintDialogImpl | CPrintDialog |
打印(Windows 2000 及之后) | PrintDlgEx() | CPrintDialogExImpl | CPrintDialogEx |
页面设置 | PageSetupDlg() | CPageSetupDialogImpl | CPageSetupDialog |
文本查找及替换 |
FindText(), ReplaceText() |
CFindReplaceDialogImpl | CFindReplaceDialog |
把这些类全写出来将会使本文相当的长,因此我只选择了前两个,也正是你在使用中经常用到的两个。
CFileDialog
CFileDialog 及其基类 CFileDialogImpl 用于显示文件打开和文件保存对话框。在 CFileDialogImpl 中最重要的两个数据成员是 m_ofn 和 m_szFileName。 m_ofn 是一个 OPENFILENAME, CFileDialogImpl 会为你对它设置一些有意义的缺省值,就像在 MFC 中一样,必要时你可以直接改变此结构中的数据。 m_szFileName 是一个 TCHAR 数组,其中为选中文件的名字。由于 CFileDialogImpl 仅有此一个字符串用于保存文件名,当你使用多选模式的打开文件对话框时你需要自己提供缓冲区。
使用 CFileDialog 的基本步骤为:
- 构造一个 CFileDialog 对象,将初始化数据传递给构造函数。
- 调用 DoModal()。
- 如果 DoModal() 返回了 IDOK,则可以从 m_szFileName 获取选定的文件。
这就是 CFileDialog 的构造函数:
1 2 3 4 5 6 7 |
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() 以显示该对话框),为 false 则创建一个文件保存对话框( CFileDialog 将调用 GetSaveFileName())。其余的参数会直接保存到 m_ofn 结构的对应成员中,不过它们都是可选参数,因为你可以在调用 DoModal() 之前直接访问 m_ofn。
与 MFC 的 CFileDialog 相比,一个显著的不同在于 lpszFilter 参数必须是一个以 NULL 字符分割的字符串列表(也这是在 OPENFILENAME 的文档中公开的格式),而不是使用管道字符分割的列表。
下面是一个使用 CFileDialog 的例子,应用了选择 Word 12 文件( *.docx)的过滤器:
1 2 3 4 5 6 7 |
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 参数。一眼望去,过滤器字符串还会有一点阅读上的困难。有两种解决办法,要么在调用 DoModal() 之前设置 m_ofn,要么从 CFileDialogImpl 派生一个新类,在其中作我们所希望的改进。在这儿我们介绍第二种,生成一个作了如下改动的新类:
- 构造函数中的字符串参数用 _U_STRINGorID 取代 LPCTSTR。
- 像 MFC 那样,过滤器字符串使用管道符分割各个域,而不是 NULL 字符。
- 对话框相对于其父窗口自动居中。
我们从写一个构造函数的接收参数类似于 CFileDialogImpl 的构造函数的类开始:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 成员,必要时加载字符串:
1 2 3 4 5 6 7 8 9 |
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) { } |
请注意,字符串参数在对基类的构造函数的调用里全部为 NULL。这是因为基类的构造函数总是在成员的初始化之前调用。为了能设置 m_ofn 中的字符串数据,我们加入了一些代码,这些代码重复了 CFileDialogImpl 的构造函数做的初始化步骤:
1 2 3 4 5 6 7 8 9 |
CMyFileDialog::CMyFileDialog(...) { <b> 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 ); </b>} |
PrepFilterString() 是一个辅助方法,它接收一个以管道符分割的过滤器字符串,将管道符改为 NULL 字符,并返回一个指向字符串起始位置的指针。其结果就是一个可以用在 OPENFILENAME 中的具有正确格式的字符串列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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() 处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class CMyFileDialog : public CFileDialogImpl<CMyFileDialog> { public: // Construction CMyFileDialog(...); <b>// Maps BEGIN_MSG_MAP(CMyFileDialog) CHAIN_MSG_MAP(CFileDialogImpl<CMyFileDialog>) END_MSG_MAP() // Overrides void OnInitDone ( LPOFNOTIFY lpon ) { GetFileDialogWindow().CenterWindow(lpon->lpOFN->hwndOwner); }</b> protected: LPCTSTR PrepFilterString ( CString& sFilter ); CString m_sDefExt, m_sFileName, m_sFilter; }; |
关联到 CMyFileDialog 对象的窗口实际上是文件打开对话框的一个子窗口,由于我们需要顶级的窗口,所以我们调用了 GetFileDialogWindow() 来获得它。
CFolderDialog
CFolderDialog 及其基类 CFolderDialogImpl 用于显示一个浏览文件夹对话框。尽管该对话框支持浏览外壳名字空间内的任何地方,但 CFolderDialog 仅具有在文件系统内浏览的能力。 CFolderDialogImpl 中两个最重要的数据成员是 m_bi 和 m_szFolderPath。 m_bi 是一个由 CFolderDialogImpl 管理并将之传递到 SHBrowseForFolder() API 中去的 BROWSEINFO,如果必要,你可以直接改变该结构中的数据。 m_szFolderPath 是一个 TCHAR 数组,用来保存选定文件夹的名字。
使用 CFolderDialog 的基本步骤是:
- 构造一个 CFolderDialog 对象,传递一些初始化数据到构造函数中去。
- 调用 DoModal()。
- 如果 DoModal() 返回 IDOK,从 m_szFolderPath 中获取选定的文件夹的路径。
下面是 CFolderDialog 的构造函数:
1 2 3 4 |
CFolderDialog::CFolderDialog ( HWND hWndParent = NULL, LPCTSTR lpstrTitle = NULL, UINT uFlags = BIF_RETURNONLYFSDIRS ) |
hWndParent 是浏览对话框的属主窗口。你既可以在构造函数中指定,也可以在 DoModal() 调用时指定。 lpstrTitle 是一个显示于对话框里的树控件之上的字符串。 uFlags 是控制对话框行为的标志,应该总是包括 BIF_RETURNONLYFSDIRS 以使树仅显示文件系统目录。在 BROWSEINFO 的文档里列出了其他可用于 uFlags 的值,不过要记住,有的标志可能会产生不太好的结果,比如 BIF_BROWSEFORPRINTER。UI 相关的标志,如 BIF_USENEWUI,可以良好工作。注意,就像在 CFileDialog 的构造函数里的字符串一样, lpstrTitle 参数也有相同的使用性问题。
下面是使用 CFolderDialog 选择一个目录的例子:
1 2 3 4 5 6 |
CString sSelectedDir; CFolderDialog fldDlg ( NULL, _T("Select a dir"), BIF_RETURNONLYFSDIRS|BIF_NEWDIALOGSTYLE ); if ( IDOK == fldDlg.DoModal() ) sSelectedDir = fldDlg.m_szFolderPath; |
为了演示定制的 CFolderDialog,我们将从 CFolderDialogImpl 派生一个类并设定其初始的选择。此对话框的回调不使用窗口消息,因此我们代之以覆盖 OnInitialized() 方法,该方法会在基类接收到 BFFM_INITIALIZED 通知时被调用。 OnInitialized() 调用了 CFolderDialogImpl::SetSelection() 来改变对话框中的选择。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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; } <b>// Overrides void OnInitialized() { // Set the initial selection to the Windows dir. TCHAR szWinDir[MAX_PATH]; GetWindowsDirectory ( szWinDir, MAX_PATH ); SetSelection ( szWinDir ); }</b> protected: CString m_sTitle; }; |
其他有用的类和全局函数
Struct 的封装
WTL 中的 CSize, CPoint 和 CRect 分别封装了 SIZE, POINT 和 RECT。它们与其 MFC 等价类的工作是一样的。
处理双类型参数的类
正如前面提到的,你可以使用 _U_STRINGorID 类型作为一个函数的参数,该参数既可以是数字也可以是字符串资源 ID。另外还有两个做类似工作的类:
- _U_MENUorID:此类型可以从一个 UINT 或者 HMENU 构造得来,主要用于 CreateWindow() 的封装层里。 CreateWindow() 的 hMenu 参数在即将创建的窗口是一个子窗口时实际上是一个窗口 ID,而 _U_MENUorID 则隐藏了这两种用法的区别。 _U_MENUorID 有一个成员 m_hMenu,它可以被作为 hMenu 参数传递给 CreateWindow() 或者 CreateWindowEx()。
- _U_RECT: 此类型可以从一个 LPRECT 或者 RECT& 构造得来,使得调用者可以传递一个 RECT,或者一个指向 RECT 的指针,再或者一个像 CRect 这样的封装类,只要它提供了到 RECT 的转换。
就像 _U_STRINGorID 一样, _U_MENUorID 和 _U_RECT 总会被你所使用的其他头文件包进来。
其他工具类
CString
WTL 的 CString 工作起来和 MFC 的 CString 一样,因此我在这儿不会详细介绍它。当你在定义了 _ATL_MIN_CRT 的情况下编译时,WTL 的 CString 使用了好多个额外的方法。这些方法,诸如 _cstrchr(), _cstrstr(),是那些当 _ATL_MIN_CRT 被定义时缺失的 CRT 函数的对应替代品。
CFindFile
CFindFile 封装了 FindFirstFile() 和 FindNextFile() API,而且比 MFC 的 CFileFind 更好用一点。常见的使用模式是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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,则至少有一个文件匹配了条件。在 do 循环的内部,你可以访问公用的 CFindFile 成员 m_fd,那是一个 WIN32_FIND_DATA 结构,其中保存有找到的文件相关的信息。循环会一直持续到 FindNextFile() 返回 false,这表示所有的文件已经枚举完成。
CFindFile 还有以更易于使用的形式从 m_fd 返回数据的方法。这些方法仅当在对 FindFirstFile() 或者 FindNextFile() 进行了成功调用之后才能返回有意义的值。
1 |
ULONGLONG GetFileSize() |
以 64 位无符号整数的形式返回文件的大小。
1 2 |
BOOL GetFileName(LPTSTR lpstrFileName, int cchLength) CString GetFileName() |
返回找到文件的文件名和扩展名(从 m_fd.cFileName 中复制)。
1 2 |
BOOL GetFilePath(LPTSTR lpstrFilePath, int cchLength) CString GetFilePath() |
返回找到文件的全路径。
1 2 |
BOOL GetFileTitle(LPTSTR lpstrFileTitle, int cchLength) CString GetFileTitle() |
仅返回找到文件的文件标题,也即不带扩展名的文件名。
1 2 |
BOOL GetFileURL(LPTSTR lpstrFileURL, int cchLength) CString GetFileURL() |
创建一个包含文件全路径的 file:// URL。
1 2 |
BOOL GetRoot(LPTSTR lpstrRoot, int cchLength) CString GetRoot() |
返回包含文件的目录。
1 2 3 |
BOOL GetLastWriteTime(FILETIME* pTimeStamp) BOOL GetLastAccessTime(FILETIME* pTimeStamp) BOOL GetCreationTime(FILETIME* pTimeStamp) |
这些方法分别从 m_fd 中返回 ftLastWriteTime、 ftLastAccessTime 和 ftCreationTime 成员。
CFindFile 还有一些辅助方法,用于检测找到文件的属性。
1 |
BOOL IsDots() |
如果找到的文件是“ .”或者“ ..”目录则返回 true。
1 |
BOOL MatchesMask(DWORD dwMask) |
把 dwMask 中的位(都应该是 FILE_ATTRIBUTE_* 常量)与找到文件的属性进行比较。如果 dwMask 中所有的位也都在文件属性中则返回 true。
1 2 3 4 5 6 7 8 |
BOOL IsReadOnly() BOOL IsDirectory() BOOL IsCompressed() BOOL IsSystem() BOOL IsHidden() BOOL IsTemporary() BOOL IsNormal() BOOL IsArchived() |
这些方法是使用某一特定 FILE_ATTRIBUTE_* 位对 MatchesMask() 进行调用的捷径。例如, IsReadOnly() 即 MatchesMask(FILE_ATTRIBUTE_READONLY)。
全局函数
WTL 还有好几个有用的全局函数,你可以用来做诸如 DLL 版本检查以及显示消息框之类的事情。
1 |
bool AtlIsOldWindows() |
如果操作系统为 Windows 95、98、NT 3 或者 NT 4 则返回真。
1 |
HFONT AtlGetDefaultGuiFont() |
返回 GetStockObject(DEFAULT_GUI_FONT) 的返回值。在英文(以及其他的使用拉丁字母的单字节语言)Windows 2000 及之后,此字体的字面名称为“MS Shell Dlg”。这在用作对话框字体时是可以的,但如果你在 UI 中要使用自己创建的字体就未必是最佳选择。MS Shell Dlg 是 MS Sans Serif 的一个别名,而不是新的 UI 字体 Tahoma 的别名。为了避免使用 MS Sans Serif,你可以用以下代码获取消息框使用的字体:
1 2 3 4 5 |
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 的一个别名。
1 |
HFONT AtlCreateBoldFont(HFONT hFont = NULL) |
此函数创建一个给定字体的粗体版本。如果 hFont 为 NULL, AtlCreateBoldFont() 创建由 AtlGetDefaultGuiFont() 返回的字体的粗体版本。
1 |
BOOL AtlInitCommonControls(DWORD dwFlags) |
这是对 InitCommonControlsEx() API 的一个封装。它使用给定的标志初始化一个 INITCOMMONCONTROLSEX 结构,然后调用该 API。
1 2 |
HRESULT AtlGetDllVersion(HINSTANCE hInstDLL, DLLVERSIONINFO* pDllVersionInfo) HRESULT AtlGetDllVersion(LPCTSTR lpstrDllName, DLLVERSIONINFO* pDllVersionInfo) |
这两个函数到给定的模块中寻找名为 DllGetVersion() 的导出函数,如果找到了,就调用之。如果 DllGetVersion() 调用成功,它把版本信息返回到 DLLVERSIONINFO 结构里。
1 |
HRESULT AtlGetCommCtrlVersion(LPDWORD pdwMajor, LPDWORD pdwMinor) |
返回 comctl32.dll 的主版本和次版本。
1 |
HRESULT AtlGetShellVersion(LPDWORD pdwMajor, LPDWORD pdwMinor) |
返回 shell32.dll 的主版本和次版本。
1 |
bool AtlCompactPath(LPTSTR lpstrOut, LPCTSTR lpstrIn, int cchLen) |
截短文件路径使之小于 cchLen 个字符的长度,如果路径太长则在其尾部添加省略号。作用与 shlwapi.dll 中的 PathCompactPath() 和 PathSetDlgItemPath() 函数相仿。
1 2 3 |
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)通常需要使用 CRT 函数,因此会有用以代替从 CRT 导入的代码的特定代码被编译进来。
- _ATL_VER
- 对于 VC 6 被预定义为 0x0300,VC 7 为 0x0700,VC 8 为 0x0800。
- _WIN32_WCE
- 如果当前的编译是用于 Windows CE 则被定义。有的 WTL 代码在相应的特性在 CE 上不可用时会被禁用。
下列宏缺省并不定义。要使用它们的话,应该在 stdafx.h 文件中所有的 #include 语句前 #define。
- _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
- 定义此符号以阻止自动执行 using namespace WTL 指令。
- _WTL_NO_AUTO_THEME
- 定义此符号可以禁止 CMDICommandBarCtrlImpl 使用 XP 主题。
- _WTL_NEW_PAGE_NOTIFY_HANDLERS
- 定义此符号可以在 CPropertyPage 中使用更新的 PSN_* 通知处理器。由于老的 WTL 3 处理器已经过时,因此应该总是定义此符号,除非你在维护不能更新 WTL 3 代码。
- _WTL_NO_WTYPES
- 定义此符号可以禁止定义 WTL 版本的 CSize, CPoint 和 CRect。
- _WTL_NO_THEME_DELAYLOAD
- 当使用 VC 6 编译时,定义此符号可以禁止 uxtheme.dll 被自动标记为延迟加载的 DLL。
注意:如果既没有定义 _WTL_USE_CSTRING 也没有定义 _WTL_NO_CSTRING,那么 CString 可以在包含了 atlmisc.h 之后的任何地方使用。
示例工程
本文的示例工程为一个名为 Kibbles 的下载器应用程序,它演示了本文介绍到的许多个类。它使用了在 Windows 2000 及其之后的系统中可以得到的 BITS(后台智能传输服务)组件。由于此应用只能运行在基于 NT 的操作系统上,所以我把它做成了一个 Unicode 工程。
应用的一个视图窗口显示了下载进度,使用了若干个 GDI 调用,包括绘制了饼状图表的 Pie()。应用首次运行时,你可以看到其初始状态的 UI:
你可以从浏览器中拖动一个链接到窗口中以创建一个新的 BITS 任务,该任务将把链接的目标下载到你的“我的文档”文件夹。你也可以点击第三个工具栏按钮来添加你想用的任意 URL。第四个按钮让你可以改变缺省的下载目录。
当下载任务正在进行时,Kibbles 会显示此任务的一些细节,并像如下这样显示下载进度:
工具栏上的前两个按钮可以让你改变进度显示中使用的颜色。第一个按钮打开一个选项对话框,在那儿你可以为显示的不同部分分别设置颜色:
对话框使用的精品按钮类来自于 Tim Smith 的文章 Color Picker for WTL with XP themes。查看 Kibbles 工程中的 CChooseColorsDlg 类可以看到其工作原理。Text color 按钮是一个普通按钮, OnChooseTextColor()处理器演示了如何使用 WTL 的 CColorDialog 类。第二个工具栏按钮会把所有的颜色改变为随机值。
第五个按钮可以让你设置一个背景图片,它会被绘制到显示已经下载了多少的饼图里。缺省的图片是作为资源包含的,但如果你的“我的图片”文件夹中有 BMP 文件,你也可以从中选择一个。
CMainFrame::OnToolbarDropdown() 包含了处理按钮按下事件并显示一个弹出菜单的代码。此函数还使用 CFindFile 来枚举“我的图片”目录下的内容。你可以到 CKibblesView::OnPaint() 中去看完成绘制 UI 的各种 GDI 操作。
关于工具栏的一个重要提示:工具栏使用了一个 256 色的位图,但是 VC 的工具栏编辑器仅能针对 16 色的位图工作。如果你使用编辑器编辑工具条,VC 会把位图减少到 16 色。我的建议是,把高色版本的位图存放到另一个目录里,用图形程序直接对它进行修改,然后保存一个 256 色的版本到 res 目录。
Copyright and License
This article is copyrighted material, (c)2006 by Michael Dunn. I realize this isn’t going to stop people from copying it all around the ‘net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don’t foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.
With the exception of ColorButton.cpp and ColorButton.h, the demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don’t make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefitting from my code) but is not required. Attribution in your own source code is also appreciated but not required.
The files ColorButton.cpp and ColorButton.h come from Color Picker for WTL with XP themes by Tim Smith. They are not covered by the above license statement; see the comments in those files for their license.
修订历史
2006 年 1 月 8 日,首次发布。