Part IX - GDI Classes, Common Dialogs, and Utility Classes

原作 :Michael Dunn

翻译 :Orbit(桔皮干了)

本章内容

对第九部分的介绍

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就是扮演一个转换资源标识类型的角色,它隐藏了两种类型的区别,函数调用者只需要直接传递UINTLPCTSTR 就可以了,这个函数在必要的时候使用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_PENBLACK_PEN, 等等),然后直接调用GetStockObject()

使用通用对话框

WTL还提供了一些类用于简化对Win32 通用对话框地使用,这些类响应通用对话框发送的消息和回调函数,依次调用重载的消息处理函数。这种设计方式和属性页非常相似,你需要为每个属性页提供单独的通知消息响应函数(比如,OnWizardNext() 处理 PSN_WIZNEXT),它们在必要的时候由CPropertyPageImpl调用。

WTL 为每个通用对话框提供两个类,例如:选择文件夹对话框由CFolderDialogImplCFolderDialog两个类封装。如果你想改变它们的默认行为或为某个消息定制一个独特的响应函数,就需要从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_ofnm_szFileNamem_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

CFolderDialogCFolderDialogImpl类用来显示一个浏览文件夹的对话框,Windows的文件夹浏览对话框能够查看整个外壳名字空间(shell namespace)的任何位置,但是CFolderDialog,只支持浏览文件文件系统。CFolderDialogImpl中最重要的两个数据成员是m_bim_szFolderPathm_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 也对SIZEPOINTRECT数据结构进行了封装,分别是CSizeCPointCRect类。

处理双类型参数的类

就像前面提到的那样,你可以使用_U_STRINGorID 去自适应那些参数是数字或字符串资源ID的函数,WTL中还有两个类和这个类的作用类似:

  • _U_MENUorID: 这个类型支持UINTHMENU,通常用在CreateWindow() 的封装中函数中,hMenu 参数在某些情况下是菜单句柄,但是在创建子窗口时它又是一个窗口ID,_U_MENUorID 用来消除(隐藏)这些差异,_U_MENUorID 有一个m_hMenu成员,用来向CreateWindow()CreateWindowEx()传递hMenu参数。
  • _U_RECT: 这个类可以从LPRECTRECT&构建,可以将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的标准函数,比如strstrstrchr等等,在这种情况下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的函数,这些函数的返回值只在成功调用了FindFirstFileFindNextFile()之后才有意义。

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的数据成员ftLastWriteTimeftLastAccessTimeftCreationTime 复制数据。

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 已经改名为 CIdleHandlerDoUpdate()也改名为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封装的CSizeCPointCRect类。

_WTL_NO_THEME_DELAYLOAD

在VC6中编译代码时,定义这个标号将阻止uxtheme.dll 被自动标记为延时加载。

注意: 如果_WTL_USE_CSTRING 和_WTL_NO_CSTRING 同时定义,产生的结果就只能在atlmisc.h 文件包含之后使用CString

例子工程

本文的演示工程是一个下载工具,这个名为Kibbles的下载工具演示了几个本文介绍的类。这个下载工具使用了BITS(background intelligent transfer service)组件,Windows 2000及其以后的操作系统都支持这个组件,也就是说这个程序只能运行在基于NT技术的操作系统上,所以我就将其创建成了Unicode工程。

这个程序有一个视图窗口,这个视图窗口用来显示下载过程,它使用了很多GDI函数,也包括专门画饼图的Pie()函数。第一次运行时,程序的初始界面是这样的:

你可以从浏览器中拖一个链接到这个窗口中,程序会创建一个新的BITS并将链接指定的目标下载到“我的文档”文件夹。当然也可以单击工具栏上第三个按钮直接添加一个URL,工具栏上的第四个按钮则允许你修改默认的下载文件存放位置。

当一个下载任务正在进行,Kibbles会显示一些下载任务的细节,下载的过程显示如下: 工具栏上的前两个按钮用来修改过程显示的颜色,第一个按钮会打开一个选项对话框,在选项对话框中可以设置过程饼图中各部分的颜色:

对话框中使用了Tim Smith的文章“Color Picker for WTL with XP themes”中介绍的一个很棒的按钮类,可以查看Kibbles工程中的CChooseColorsDlg 类的代码了解这个按钮类是如何工作的。“Text color” 按钮是一个普通的按钮,它的响应函数OnChooseTextColor()演示了如何使用WTL的CColorDialog类。第二个工具栏按钮的功能是使用随即颜色显示下载过程。

第五个工具栏按钮用来设置背景图片,Kibbles使用这个图片在饼图上显示下载过程。默认的图片程序资源中包含的一个图片,你也可以选择任何BMP位图文件作为背景图片:

CMainFrame::OnToolbarDropdown() 的代码响应按钮的press事件并显示一个弹出式菜单,这个函数还使用了CFindFile 类遍历“我的文档”文件夹。关于各种GDI函数的用法可以查看CKibblesView::OnPaint()函数的代码。

关于工具栏有一点需要特别注意:这个工具栏使用了256色的位图,不过VC的工具栏编辑器只支持16色位图,如果你使用工具栏编辑器修改过工具栏位图,你的工具栏位图就会被转换成16色。我的建议是,在另一个目录中保存这个高彩色的位图,使用图像编辑工具直接编辑这个图片,然后在“res”目录中另存一个256色的版本。

版权和许可协议

这篇文章受版权保护, (c)2006 by Michael Dunn。我知道不能阻止人们通过网络拷贝这些文章,但是我必须要说的是,如果你有兴趣翻译本系列文章,请一定让我知道(汗....,还好翻译第一篇之前给Michael发了一封邮件),我不会拒绝你的翻译请求,我只是想知道我的文章被翻译了几个版本,这样我可以在这里给出相应版本的链接。

有两个文件除外,就是ColorButton.cppColorButton.h,这篇文章附带的演示代码是向所有人公开的,这样每个人都可以从代码中受益。 (我不让文章也向所有人(版权)公开是因为这样能够提高我自己和CodeProject网站的知名度(%……¥%¥#%¥#晕)。) 如果你的程序中使用了我的演示代码,最好能给我发个Email,当然这不是强制要求,只是为了满足我的一点好奇心,我想知道是否有人从我的代码中受益。

文件ColorButton.cppColorButton.h 来自Tim Smith的文章“Color Picker for WTL with XP themes”,它们不包含在上面的许可声明中,它们的许可声明包含在文件的注释部分。

修订历史

2006年2月8日,第一次发布。