# 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會顯示一些下載任務的細節,下載的過程顯示如下:  工具欄上的前兩個按鈕用來修改過程顯示的顏色,第一個按鈕會打開一個選項對話框,在選項對話框中可以設置過程餅圖中各部分的顏色:

對話框中使用了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位圖文件作為背景圖片:

`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日,第一次發布。
- 中文版序言
- Part I - ATL GUI Classes
- Part II - WTL GUI Base Classes
- Part III - Toolbars and Status Bars
- Part IV - Dialogs and Controls
- Part V - Advanced Dialog UI Classes
- Part VI - Hosting ActiveX Controls
- Part VII - Splitter Windows
- Part VIII - Property Sheets and Wizards
- Part IX - GDI Classes, Common Dialogs, and Utility Classes
- Part X - Implementing a Drag and Drop Source