第三章 操作檔案
我依然清楚地記得,Windows95 的Beta版出現的情形,它在朋友之間和學院中傳播,好酷,全新的檔案管理器,一種全圖示,全彩色可客戶化的界面,以及活潑的動畫辨別使得在檔案拷貝和删除方面的操作更容易和直覺。
作為真正的軟體狂人,我們能為一個比薩餅的獎金開始競賽,一直以求成為第一個能夠程式設計再造如此行為的人—即,怎樣以動畫方式拷貝檔案。花了幾個小時的時間才在一大堆新函數中找出了SHFileOperation()函數,這是一個響應動畫拷貝的API函數,它也是探測器執行所有檔案操作的函數。
競賽的規則之一是建立一個具有這個唯一目标功能的示範程式。在這個函數出現之後,這個問題實際上是十分簡單的。事實上,當我确定在程式中使用這個函數作為标準函數來進行檔案操作時,問題就出現了。要這樣做,你就必須徹底弄清楚這個函數的原型和它的能力,實際有趣的故事從這裡就開始了。
在這一章中,我打算向你展示SHFileOperation()的内部奧秘。
怎樣正确地使用函數所支援的标志和指令
怎樣正确使用源/目緩沖區
最有可能的傳回碼是什麼
對于長檔案名,可能遇到的問題
關于檔案名映射,以前未暴露的問題
與這本書的其它任何地方一樣,在這一章中,你将發現一些有幫助的函數,它們推動你使用Windows的通用控件,對話框。
SHFileOperation()能做些什麼
要得到這個問題的答案,先讓我們先來看一下在檔案shellapi.h中SHFileOperation()函數的聲明:
int WINAPI SHFileOperation(LPSHFILEOPSTRUCT lpFileOp);
進一步,看一看SHFILEOPSTRUCT結構,這也是一個在shellapi.h中定義的結構。
typedef struct _SHFILEOPSTRUCT
{
HWND hwnd;
UINT wFunc;
LPCSTR pFrom;
LPCSTR pTo;
FILEOP_FLAGS fFlags;
BOOL fAnyOperationsAborted;
LPVOID hNameMappings;
LPCSTR lpszProgressTitle;
} SHFILEOPSTRUCT, FAR* LPSHFILEOPSTRUCT;
通過這個結構,SHFileOperation()函數可以做任何想要做的操作。簡要地說,這個函數可以做:
把一個或多個檔案從源路徑拷貝到目路經
删除一個或多個檔案,把它們發送到‘資源回收筒’
重命名檔案
把一個或多個檔案從源路徑移動到目路徑
到目前為止,我們沒有看到任何新東西—至少沒有特别刺激的東西。事實上,Win32 API(和C運作庫)已經提供了做同樣事情的方法。特别是Win32 API提供了 CopyFile(), DeleteFile(), 和MoveFile()來執行這些任務。
然而,強大的SHFileOperation()函數的出現,使你能夠僅僅使用一個指令就可以處理對預設目錄的多重拷貝和建立。他還支援‘Undo’操作,以及在目标名沖突的情況下自動重命名操作。最後,他還大方地提供了一個空白紙頁一個從檔案夾漂動到另一個檔案夾顯示的動畫。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIw1mYuMTMfxGblh2cvwFa6h2YoN2LcRXZu9lbkN3Yfd2bsJ2Xw9CXzV2Zh1WavwFdl5mLuR2cj5yZvxmYtA3Lc9CX6MHc0RHaiojIsJye.bmp)
毋庸置疑,你可以從Win32的底層APIs獲得同樣的功能,但是這可能需要做大量的工作。
SHFileOperation()函數怎樣工作
與所有僅使用資料結構作為輸入參數的函數一樣,SHFileOperation()函數是一個相當靈活的例程。通過以适當的方式組合各種标志,和使用(或不使用)各個SHFILEOPSTRUCT結構的成員,它可以執行許多操作。 下面就讓我們來看一看這個結構中每一個成員所起的的作用:
名 | 描述 |
Hwnd | 由這個函數生成的所有對話框的父視窗Handle。 |
wFunc | 表示要執行的操作 |
pFrom | 含有源檔案名的緩沖 |
pTo | 含有目标檔案名的緩沖(不考慮删除的情況) |
fFlags | 能夠影響操作的标志 |
fAnyOperationsAborted | 包含TRUE或FALSE的傳回值。它依賴于是否在操作完成之前使用者取消了操作。通過檢測這個成員,你就可以确定操作是正常完成了還是被手動中斷了。 |
hNameMappings | 資料描述它為包含SHNAMEMAPPING結構數組的檔案名映射對象的Handle。 |
lpszProgressTitle | 一個在一定情況下用于顯示對話框标題的字元串。 |
簡言之,有四個成員确實需要進一步研究,它們是:
wFunc (間接地包括pFrom和pTo)
fFlags
hNameMappings
lpszProgressTitle
可用的操作
wFunc成員指定了在給定檔案上操作,這些檔案由pFrom和pTo給出。wFunc的可能取值(在shellapi.h定義)是:
代碼 | 值 | 描述 |
FO_MOVE | 0x0001 | 所有在pFrom中指定的檔案都被移動到pTo指定的位置,pTo必須是一個目錄名。 |
FO_COPY | 0x0002 | 所有在pFrom中指定的檔案都被拷貝到pTo指定的位置,其内容可以是目錄名或甚至是一個與pFrom 1:1對應的檔案集。 |
FO_DELETE | 0x0003 | 所有在pFrom中指定的檔案都被發送到‘資源回收筒’,pTo被忽略。 |
FO_RENAME | 0x0004 | 所有在pFrom中指定的檔案都重新命名為pTo中指定的名字,在pFrom和pTo之間,名字不需1:1對應。 |
pFrom和pTo都是包含一個或多個檔案名的緩沖。如果包含了多于一個的檔案名,則各個檔案名之間就需要用NULL(字元/0)進行分隔,并且整個串需要用兩個NULL(/0/0)字元結束,無論有多少檔案名。
如果pFrom和pTo不包含目錄資訊(即,它們不是全路徑名),則,函數假設它應該使用由GetCurrentDirectory()函數傳回的驅動器和目錄。pFrom可以包含通配符,也可以是“*.*”這樣的字元串。
設定SHFILEOPSTRUCT結構的fFlags成員标志能夠影響所有這些操作。線上資料中按字元順序列出了所有标志。在我們的簡短讨論中,将采取稍微不同的方法,将标志根據它能影響的實際操作分組,如果你想要自然排列的表,請引用線上資料。
注意兩個空的結尾符(/0/0)
其實,就pFrom和pTo是指向一個字元串清單的指針而不是通常意義的緩沖這樣一個事實而言,資料的說明并不充分。也就是說,SHFileOperation()總是期望傳送來的串由兩個NULL字元終止,即使你傳送的隻有單個檔案名或使用通配符的單個串也是如此。如果不使用兩個NULL字元來終止pFrom和PTo中的字元串,則可能的情況就是函數在分析傳來的内容時失敗。此時,它傳回一個‘不能拷貝/移動檔案’錯(錯誤碼 1026)。沒有兩個NULL字元,函數可能會把字元串尾,單個NULL字元後的位元組作為被拷貝或移動的檔案名。這些位元組可以是任何東西,可能不是合法的檔案名,是以錯誤就出現了。由于pFrom總是被解釋為檔案名清單,而pTo隻有在FOF_MULTIDESTFILES标志下才被解釋為檔案名清單,是以這個錯誤常常伴随pFrom一同出現。在所有其它情況,SHFileOperation()都假設pTo引用單個檔案名。是以單個NULL字元終止是充分的—兩個NULL終止僅僅在終止包含多個檔案名的清單時被要求。除非明确說明有多個目标檔案,對pTo内容的解析停止于頭一個NULL終止符。
解析方法依賴于指針是否引用了字元串清單或簡單緩沖,為安全起見,你應該總附加一個終止符到你打算指派給pFrom的字元串後面,同樣,對pTo,如果有多個目的檔案的話,也是如此。字面上,你可以顯式加一個/0在串的結尾(當然,字元串自動終止在單個NULL字元上):
shfo.pFrom = "c://demo//one.txt/0c://demo//two.txt/0";
如果使用變量,可以采用下面的方法:
pszFrom[lstrlen(pszFrom) + 1] = 0;
移動和拷貝檔案
要把檔案從一個位置移動或拷貝到另一個位置,需要指定:
包含源檔案名的緩沖。可以是一個名字序列,單個名字,一個包含通配符的
串,甚至可以是含通配符的串序列。
一個目的目錄。如果你移動一個确定的檔案清單,還要準備一個目标名清單,
注意保證1:1的與源名對應。換句話說,每一個源檔案名都
必須有一個目标檔案名以便移動或拷貝。如果有多個目标文
件,就必須在fFlags中指定FOF_MULTIDESTFILES标志。
這個标志可以影響的操作是:
标志 | 值 | 描述 |
FOF_MULTIDESTFILES | 0x0001 | pTo成員包含多個與源檔案對應的目标檔案。 |
FOF_SILENT | 0x0004 | 發生的操作不需要傳回到使用者,就是說,不顯示進度條對話框,而其它相關的消息框仍然顯示。 |
FOF_RENAMEONCOLLISION | 0x0008 | 如果目标位置已經包含了與打算移動或拷貝的檔案重名的檔案,這個标志訓示要自動地改變目标檔案。 |
FOF_NOCONFIRMATION | 0x0010 | 這個标志使函數對任何消息框的回答總是Yes,隻有一個例外,就是當詢問是否建立預設目錄的對話框顯示時。此時,需要FOF_NOCONFIRMMKDIR标志幫忙。(參考後面的說明)。 |
FOF_FILESONLY | 0x0080 | 這個标志僅僅應用于指定了包含子目錄和通配符(*.*)的情況。設定了這個标志,函數僅僅處理檔案而不進入到子目錄。 |
FOF_SIMPLEPROGRESS | 0x0100 | 這個标志産生一個簡化的使用者界面:有一個動畫視窗,但是不顯示檔案名,而是顯示通過lpszProgressTitle 成員指定的文字。 |
FOF_NOCONFIRMMKDIR | 0x0200 | 如果目标目錄不存在,這個标志使函數默默地建立一個預設目錄。沒有這個标志,函數将提示是否建立一個完整的目的路徑。這個标志與下一個将要介紹的标志有點微妙的關系。 |
FOF_NOERRORUI | 0x0400 | 如果設定了這個标志,發生的任何錯誤都不會引起消息框的顯示,全部都傳回錯誤碼。這個标志與上一個标志關系有點微妙。 |
FOF_NOCOPYSECURITYATTRIBS | 0x0800 | 應用于WindowsNT,Shell4.71(WindowsNT具有IE4.0 和活動桌面),和更高版本。這個标志防止對具有安全屬性的檔案進行拷貝。 |
現在讓給我們更詳細地了解一下這些選擇,在移動或拷貝檔案的時候,所關心的有兩個主要方面:正确地辨別要傳送的檔案,和確定所設定的标志産生所希望的行為。
避免不想要的對話框
如果你希望操作默默地進行,不需要顯示對話框或系統錯誤消息,你可能認為FOF_NOERRORUI | FOF_SILENT标志的組合是一個好的選擇。然而,這并不是真的,正象我所提到的,使用FOF_NOERRORUI僅僅能隐藏錯誤引發的消息框。另一方面,FOF_SILENT标志自己不能防止這個函數顯示所有可能的消息框。事實上,FOF_SILENT僅僅影響到進度條對話框—即,顯示被拷貝或移動的檔案名,伴随一個通常的動畫對話框。如果函數發現給定的檔案或目錄在目标位置已經存在,它将總是顯示提示。要避免這個行為,你就需要把FOF_NOCONFIRMATION設定加到标志中。這将使函數在每一步都采用一個不可見的Yes點選行為。然而這個故事遠沒有結束。
如果目标路徑包含了預設目錄,所有這些标志都無效。在繼續執行檔案的拷貝或移動之前,這個函數試圖保證目标目錄的存在,你可能已經合理地指定了一個不存在的目錄,這個函數将小心地建立它,但是,它首先要求一個顯式的認可。
要跳過這個對話框,需要設定标志FOF_NOCONFIRMMKDIR。如果設定了這個位,函數就自動建立任何預設的目錄而不顯示提示框。
概括地說,如果想完成拷貝(或移動)操作而不需要使用者的幹涉,你可以使用如下的标志組合設定SHFILEOPSTRUCT 結構的fFlags成員:
FOF_SILENT
FOF_NOCONFIRMATION
FOF_NOERRORUI
FOF_NOCONFIRMMKDIR
然而,關于同時使用FOF_NOERRORUI和FOF_NOCONFIRMMKDIR标志組合,仍然有一點是需要澄清的。
預設目錄
有趣的是,一個預設目錄可以看作是一個由系統對話框彈出的系統錯。盡管你可以通過設定FOF_NOCONFIRMMKDIR标志跳過這個對話框,但是FOF_NOERRORUI标志優先于FOF_NOCONFIMMKDIR,有效地抑制了對話框,使後面所涉及到它的标志不被選擇。如果這兩個标志都被指定,你既不能被提示授權建立不存在的目錄,也不能自動建立目錄,相反,這個函數繼續執行就象拒絕建立目錄一樣,并将傳回:
錯誤碼117
取消标志fAnyOperationsAborted設定到True
不産生檔案的移動或拷貝
這是否是說,要避免使用FOF_NOERRORUI标志呢?當然,如果你想要絕對靜默的操作,就不可避免地要使用它—以防止所有錯誤消息框顯示。問題是它也阻止了新目錄預設地建立,并且産生一個無謂而又麻煩的錯誤。幸運地是,有一種方法能夠繞過它,即,在使用這個标志調用SHFileOperation()前,確定pTo中存儲的是已存在的全路徑名。Win32提供了一個實作這個目的的函數:
BOOL MakeSureDirectoryPathExists(LPCSTR DirPath);
使用這個函數需要 #include imagehlp.h 檔案,和連接配接imagehlp.lib庫。
檔案重命名
SHFileOperation()函數在置換已存在檔案時能夠引起的問題之一是:
或類似地,它引起的已存在目錄的問題:
通過設定FOF_NOCONFIRMATION,可以隐含地允許函數置換老對象,但是第二種可能出現了。你知道,如果在Windows探測器中選擇檔案,并按Ctrl-C鍵,然後按Ctrl-V鍵,在同一個檔案夾下将出現一個新檔案,這個檔案具有同拷貝Xxxx相似的檔案名,此處Xxxx就是你選擇的檔案。探測器自動重命名了這個新檔案以避免沖突。隻要設定了FOF_RENAMEONCOLLISION标志,SHFileOperation()函數也能提供這個功能。FOF_RENAMEONCOLLISION和FOF_NOCONFIRMATION标志組合禁止了置換操作時的确認對話框。然而接下來,你的檔案或目錄将不可避免地被覆寫。如果不合理的情況下指定這兩個标志,則FOF_RENAMEONCOLLISION标志優先
标志間的關系
到目前為止,在你的腦海中應該有兩個問題,一是各個标志之間究竟是什麼樣的關系,其次是哪些标志影響哪類對話框。下表給出了問題的答案。
标志 | 抑制的對話框 | 相關性與優先級 |
FOF_MULTIDESTFILES | None | None |
FOF_FILESONLY | None | None |
FOF_SILENT | 如果設定,進度對話框不顯示。 | 優先于FOF_SIMPLEPROGRESS标志。 |
FOF_SIMPLEPROGRESS | None | 為FOF_SILENT标志所抑制。 |
FOF_RENAMEONCOLLISION | 如果設定了這個标志,當被移動或拷貝的檔案與已存在檔案同名時置換對話框不會出現。 | 名字沖突時,如果FOF_NOCONFIRMATION标志設定,則操作繼續。 如果二者都設定了,則它優先于FOF_NOCONFIRMATION。即,檔案以給定的新名字複制,而不是覆寫。 |
FOF_NOCONFIRMATION | 如果設定,确認對話框在任何情況下都不出現。 | 名字沖突時,引起檔案覆寫,除非設定了FOF_RENAMEONCOLLISION标志。 |
FOF_NOCONFIRMMKDIR | 抑制請求建立新檔案夾的對話框 | 預設目錄作為嚴重錯誤産生一個錯誤消息框。 建立目錄的确認對話框作為錯誤消息框是否顯示依賴于FOF_NOERRORUI的設定。 |
FOF_NOERRORUI | 抑制所有錯誤消息框。 | 優先于前一個标志。如果設定,則,預設目錄引起不被處理的異常,并且傳回錯誤碼。 |
一個例程
為了有助于了解SHFileOperation()函數的特性,我們給出了一個簡單的綜合例子程式,稱之為SHMove。使用VC++ 建立基于對話框的應用,下面是需要建立的使用者界面:
你可以在OnInitDialog()函數中看到預設的設定。這個函數在SHMove.cpp中聲明。
void OnInitDialog(HWND hDlg)
{
// Set the icons (T/F as to Large/Small icon)
SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));
// Initialize the 'to' and 'from' edit fields
SetDlgItemText(hDlg, IDC_TO, "c://NewDir");
SetDlgItemText(hDlg, IDC_FROM, "c://demo/0";
shfo.pTo = "c://NewDir";
另一個可能的情況是你想要拷貝多重單個檔案到同樣數目的單個檔案上。這必須滿足兩個要求,首先應該設定FOF_MULTIDESTFILES标志,其次,一定要保證每一個源檔案都有一個目的檔案—需要完備的1:1對應。原檔案清單中第n個檔案被拷貝或移動到目的檔案清單中的第n個檔案。
shfo.fFlags |= FOF_MULTIDESTFILES;
shfo.pFrom = "c://one.txt/0c://two.txt/0";
shfo.pTo = "c://New one.txt/0c://New two.txt/0";
如果哪個方面沒有滿足,哪個方面就失敗。例如執行下面的代碼:
shfo.fFlags |= FOF_MULTIDESTFILES;
shfo.pFrom = "c://one.txt/0c://two.txt/0c://three.txt/0";
shfo.pTo = "c://New one.txt/0c://New two.txt/0";
目标檔案清單的第一項(即c:/New one.txt)被作為所有源檔案要去的檔案夾名。實際上,這個操作被處理成多對一的操作了。
在使用通配符時,源緩沖可以隐含地包括檔案和目錄。如果想要函數僅處理檔案,加一個FOF_FILESONLY标志就可以了。如果想要拷貝整個目錄,就需要加0";
上面代碼企圖删除整個c:/demo目錄的内容,并且導出對話框:
就象看到的那樣,由于沒有指定FOF_ALLOWUNDO标志,消息框中沒有提到‘資源回收筒’。通過設定FOF_ALLOWUNDO标志,檔案将改為直接發送到資源回收筒:
上表列出的其他标志與拷貝或移動操作所作的完全相同。是以,可以通過設定FOF_SIMPLEPROGRESS或 FOF_SILENT隐藏正在被删除的檔案名,通過設定FOF_FILESONLY,僅僅删除檔案。注意FOF_FILESONLY标志不能進入子目錄。上面顯示的對話框也沒有提示有多少檔案要被删除。然而這是好了解的,因為發起計算的指令中包含了通配符(否則将顯示檔案數),是以函數不能得出檔案數。這也可能就是為什麼沒有檔案被删除時它傳回成功的原因吧。
按慣例,作業系統在檔案被删除前将請求确認。如果你發現這樣的對話框是無用的,你就可以通過對所有詢問回答Yes來自動地隐藏它,這隻需設定FOF_NOCONFIRMATION标志即可。典型地,一個FO_DELETE操作如下圖所示:
檔案重命名
在這一節中第一個要注意的事情就是不能用通配符來使SHFileOperation()函數重命名檔案。通過指定單個源檔案名到pFrom和單個目标檔案名到pTo來改變檔案名似乎是唯一的方法:
ZeroMemory(&shfo, sizeof(SHFILEOPSTRUCT));
shfo.wFunc = FO_RENAME;
shfo.pFrom = "c://demo//one.txt/0";
shfo.pTo = "c://demo//one.xxx";
顯然,有兩件事情在重命名檔案操作中不允許做是有道理的,明确地說,它們是:
改變目标目錄。重命名隻是改變名字,不是檔案夾。
覆寫已存在的檔案
如果努力執行這樣的操作,則自然隻能獲得錯誤。收索所有的錯誤代碼我們發現,試圖傳遞下面的參數到函數:
shfo.pFrom = "c://demo/0";
shfo.pTo = "c://newdir";
顯然是不對的,并且函數适時地傳回下面的錯誤消息:
盡管指令荒謬地傳回成功(值是0),這個消息是足夠清楚的了。然而,這個消息隐含地說明用MS-DOS所用的文法在這裡也能正常工作。換句話說,我們應該能夠重命名,如*.txt 到 *.xtt。在MD-DOS下這些是沒問題的,在SHFileOperation()下,它不行。如果你測試,将得到如下消息:
這個消息由下面的兩行代碼引起:
shfo.pFrom = "c://demo/0";
shfo.pTo = "c://NewDir";
這個函數仍然傳回0,即使沒有檔案被處理。相同的情況也出現在删除檔案操作中。即使沒有檔案被删除,傳回碼仍然表示成功。就信譽而言,這是否算做一個bug,或是一個故意的行為。沒有快捷的方法來驗證操作是否獲得了所希望的結果,是以一定要記住函數傳回之後一定要檢查檔案是否還存在。
長檔案名
盡管Windows Shell的設計和實作是要帶給使用者最大的友善,然而,其中的某些函數對于長檔案名似乎有點問題。确實,這是對的—長檔案名。下面就讓我們看看什麼是長檔案名。
在所有上面看到的樣例中,我們為目标檔案夾指定了全路徑名(通常使用c:/NewDir)。資料上說,如果沒有提供全路徑名,這個函數就假設使用由API函數GetCurrentDirectory()傳回的目前目錄。好,現在就測試一下,在函數SHFileOperation()中使用下面代碼:
shfo.pFrom = "c://demo/0";
shfo.pTo = "NewDir";
我們想要拷貝或移動c:/demo目錄下的所有檔案到一個稱之為NewDir的新的或已經存在的目錄,該目錄定位于目前目錄下。如果在傳輸的檔案中所提供的檔案名沒有長檔案名的話,所有操作都能順利地執行。如果有任何長檔案名,下面這個對話框将出現:
這個函數所發生的操作是試圖縮短長檔案名以確定它能正确地被存儲到目标驅動器。當目标機器運作在Windows3.1的情況下,在網絡上移動檔案,這樣做是非常自然的。不幸的是我們是在運作32位作業系統的單個機器上拷貝或移動檔案—這是适應長檔案名的系統。如果不縮短檔案名,函數SHFileOperation()就不工作。
奇怪的是,如果加一個驅動器到目标檔案夾上,所有事情就又能工作了。還有一個不太陌生的情況,你将驚奇地發現,使用相對路徑時,所有操作都是順利的。奇怪,究竟發生了什麼?
如果路徑名開始字元是一個可用驅動器的邏輯辨別符時,SHFileOperation()函數在長檔案名下能順利工作。否則,它認為你正在連接配接遠端驅動器,為此,支援長檔案名的檢查失敗(如果沒有N:驅動器,肯定失敗)。例如,在我的機器中,直到F都能順利工作,這是一個CD-ROM驅動器。
這可能是計算檔案的目标驅動器所使用的代碼中某個地方有錯誤而造成的,要想正常地操作,最簡單的方式就是總使用全路徑名。
檔案名映射對象
在閱讀SHFileOperation()的官方資料時,你可能已經注意到了關于檔案名映射對象的謹慎描述。特别是,在對SHFILEOPSTRUCT結構的成員hNameMappings的表述時,資料中講到了這個對象。hNameMappings是一個指向記憶體塊的指針—聲明為LPVOID,該記憶體塊中包含一定數量的SHNAMEMAPPING結構。SHNAMEMAPPING的資料結構定義如下:
typedef struct _SHNAMEMAPPING
{
LPSTR pszOldPath;
LPSTR pszNewPath;
int cchOldPath;
int cchNewPath;
} SHNAMEMAPPING, FAR* LPSHNAMEMAPPING;
這個結構辨別了被拷貝,移動甚至重命名的檔案。更精确地說,它不僅存儲了初始(全路徑)檔案名而且還有新的(全路徑)檔案名。是以,它暗示了一種可能性:在SHFileOperation()函數執行期間,你能夠獲得所發生情況的完整報告。可惜的是,事情并不象想象的那麼簡單。
首先,要使SHFileOperation()填寫hNameMappings成員,你就必須指定一個附加的标志FOF_WANTMAPPINGHANDLE。隻這樣做還不夠,因為隻有你也設定了FOF_RENAMEONCOLLISION标志,這個成員才被填寫。進一步說,要使函數填寫所有東西而不是0,檔案操作就要使用重命名來避免沖突。所有其它情況,hNameMappings 隻簡單地指向NULL。
檔案映射示例
建立一個基于對話框的應用稱之為FileMap,用以測試關于檔案映射的一些東西。這裡是使用者界面:
要使用現實的值設定對話框,和初始化清單觀察,你需要象如下方式調整OnInitDialog()函數(記住附加#include resource.h語句):
void OnInitDialog(HWND hDlg)
{
HWND hwndList = GetDlgItem(hDlg, IDC_LIST);
// 設定報告觀察
LV_COLUMN lvc;
ZeroMemory(&lvc, sizeof(LV_COLUMN));
lvc.mask = LVCF_TEXT | LVCF_WIDTH;
lvc.cx = 200;
lvc.pszText = "Original File";
ListView_InsertColumn(hwndList, 0, &lvc);
lvc.pszText = "Target File";
ListView_InsertColumn(hwndList, 1, &lvc);
// 初始化編輯區域
SetDlgItemText(hDlg, IDC_FROM, "c://thedir//*.*");
SetDlgItemText(hDlg, IDC_TO, "c://newdir");
// 設定圖示(T/F 作為大/小圖示)
SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));
}
現在可以編輯OnOK()函數了,附加的代碼說明怎樣取得和測試檔案名映射對象的Handle:
void OnOK(HWND hDlg)
{
TCHAR pszFrom[1024] = {0};
TCHAR pszTo[MAX_PATH] = {0};
GetDlgItemText(hDlg, IDC_FROM, pszFrom, MAX_PATH);
GetDlgItemText(hDlg, IDC_TO, pszTo, MAX_PATH);
SHFILEOPSTRUCT shfo;
ZeroMemory(&shfo, sizeof(SHFILEOPSTRUCT));
shfo.hwnd = hDlg;
shfo.wFunc = FO_COPY;
shfo.pFrom = pszFrom;
shfo.pTo = pszTo;
shfo.fFlags = FOF_NOCONFIRMMKDIR |
FOF_RENAMEONCOLLISION |
FOF_WANTMAPPINGHANDLE;
int iRC = SHFileOperation(&shfo);
if(iRC)
{
SPB_SystemMessage(iRC);
return;
}
// 跟蹤Handle值
Msg("hNameMappings is: %x", shfo.hNameMappings);
// 象推薦那樣釋放對象
if(shfo.hNameMappings)
SHFreeNameMappings(shfo.hNameMappings);
}
要特别注意代碼的最後一行—釋放檔案名影射對象是你能在其上執行的最簡單的操作。你必須調用SHFreeNameMapping(),并且傳遞從SHFileOperation()中接受的Handle參數。每一步都能正常地執行,并且也能很好地了解。或許有一天,Windows的資料也能如此清晰。
總之,運作這段代碼後,你将發現hNameMappings總是NULL,除非在執行的(拷貝,移動,重命名)操作中引起了名字沖突。如果發生重命名操作,這個Handle 用于向你報告已經實際重命名檔案的情況,以避免覆寫其他檔案,報告給出了檔案的新名和原名。
是以檔案名影射對象與記憶體影射檔案或其他程序内通訊的機理一樣,沒有做任何事情。它就是一個記憶體塊,允許Shell (和你)保持對已經重命名的檔案蹤迹的跟蹤,以避免名字沖突。
如果目标目錄(本例中為 c:/newdir)不存在,或它包含的檔案名全都不同于源路徑(本例中為c:/thedir/*.*)中的檔案,則不論指定了什麼标志,Handle都是NULL:
相反,如果至少有一個重命名沖突發生,這個Handle 就引用了有意義的資料塊,是以,也就傳回了可用的記憶體位址。
使用檔案映射對象
擷取檔案名映射對象的Handle隻是完成了一半工作,現在我們來評估一下,這個Handle是多麼地有用!在資料中僅簡單地說,(在非NULL時)hNameMappings指向一個SHNAMEMAPPING結構的數組。并沒有提到怎樣獲得這個數組的尺寸。更有甚者,說這個SHFileOperation()用于存儲Handle的LPVOID成員不是一個指向資料結構數組的指針。顯然,簡單地經由循環周遊數組的方法在這裡就不能工作了。
在有些舊的MSDN資料中,你将發現兩個提到的函數SHGetNameMappingCount()和 SHGetNameMappingPtr()。然而,現在這兩個函數不僅在資料中沒有說明,而且也沒有公開。在Shell32 (來自IE4.0或更高)的版本中也沒有它們的任何蹤迹。這樣就很不好了,因為它們确實是使你能正确編碼的函數。不可了解,為什麼删除了這些函數,而且對hNameMappings成員的支援顯得既生冷又陳舊。
一個未寫進資料的結構
資料說明的東西是真的,但是,是不完整的。問題在于它忽視了上面提到的落在hNameMappings和數組之間的資料結構。有兩條線索是我獲得了正确的蹤迹,第一,來自下面代碼的輸出:
TCHAR* pNM = static_cast<TCHAR*>(shfo.hNameMappings);
Msg(pNM);
在測試這段代碼時,我順利地獲得了另一種通路非法錯,奇怪的是,它正好重複了錯誤号(如 9)。這是重命名沖突錯誤号嗎?在檢查了目錄之後發現,确實是。當然我立即執行了另一個使用不同檔案數的檢測,并且驗證了這個想法。無論hNameMappings指向什麼,開始都與全體檔案名映射對象數一緻。
是以下一步的工作将是遍覽Internet用戶端SDK和MSDN文檔,探讨某些未知的剪裁闆格式,它們是:
Windows ShellAPI 和拖拽操作
MSDN的知識庫文章Q154123
這些格式(其中有一個是“檔案名映射”),在請求拷貝和粘貼操作時,或在從一個檔案夾到另一個檔案夾拖拽檔案對象時是由Shell内部使用的。更有趣的是,很多這樣的格式在剪裁闆中都是以資料塊的方式存儲的,包含了一個數字和一個指向客戶資料結構的指針。數字表示數組的尺寸,指針指向他的第一個元素。
近似的方案
高興的是同樣的模式也可以應用到了映射對象,是以,我定義了一個結構SHNAMEMAPPINGHEADER具有如下格式:
struct SHNAMEMAPPINGHEADER
{
UINT cNumOfMappings;
LPSHNAMEMAPPING lpNM;
};
typedef SHNAMEMAPPINGHEADER* LPSHNAMEMAPPINGHEADER;
這個結構實際上與hNameMappings所指向的資料有相同的格式。畫圖說明如下,這也說明了一種通路SHNAMEMAPPING資料結構的方法:
如此,寫一個函數來枚舉所有檔案名映射對象就是直接了當的事情了;我把它稱之為。SHEnumFileMapping()。在觀察函數本身之前,先要擴充一下前面的OnOK(),以包含對該函數的調用:
void OnOK(HWND hDlg)
{
...
// 跟蹤這個handle的值
Msg("hNameMappings is: %x", shfo.hNameMappings);
// 枚舉檔案映射對象
SHEnumFileMapping(shfo.hNameMappings, ProcessNM,
reinterpret_cast<DWORD>(GetDlgItem(hDlg, IDC_LIST)));
// 如推薦那樣釋放對象
if(shfo.hNameMappings)
SHFreeNameMappings(shfo.hNameMappings);
}
SHEnumFileMapping()函數接受Handle,回調過程,和通用緩沖。它枚舉所有SHNAMEMAPPING,并逐一傳送它們給回調函數,以便作進一步的處理。
int WINAPI SHEnumFileMapping(HANDLE hNameMappings, ENUMFILEMAPPROC lpfnEnum,
DWORD dwData)
{
SHNAMEMAPPING shNM;
// 檢查Handle
if(!hNameMappings)
return -1;
// 獲得結構頭
LPSHNAMEMAPPINGHEADER lpNMH = static_cast<LPSHNAMEMAPPINGHEADER>(hNameMappings);
int iNumOfNM = lpNMH->cNumOfMappings;
// 檢查函數指針; 如果NULL, 直接傳回影射數
if(!lpfnEnum)
return iNumOfNM;
// 枚舉對象
LPSHNAMEMAPPING lp = lpNMH->lpNM;
int i = 0;
while(i < iNumOfNM)
{
CopyMemory(&shNM, &lp[i++], sizeof(SHNAMEMAPPING)); if(!lpfnEnum(&shNM,
dwData))
break;
}
// 傳回實際處理的對象數
return i;
}
SHEnumFileMapping()函數與絕大多數Windows枚舉函數所遵循的模式一樣。它接受回調函數和通用緩沖,這個緩沖用于儲存調用程式傳輸給回調函數的客戶資料,此外,它期望回調函數在終止枚舉時傳回0。我定義的回調函數類型為ENUMFILEMAPPROC:
typedef BOOL (CALLBACK *ENUMFILEMAPPROC)(LPSHNAMEMAPPING, DWORD);
這個函數接受一個SHNAMEMAPPING對象的指針,和調用程式發送的客戶資料。
當然使用類枚舉函數列出所有結構是一個個人偏好。等價地也可以使用導航界面,提供FindFirstSHNameMapping()和FindNextSHNameMapping()函數。
事實上,由回調函數執行這個操作要好得多。在這裡我所使用的(ProcessNM())是從任何它所接收的SHNAMEMAPPING結構中抽取pszOldPath和 pszNewPath字段值。并且把它們加到報告的清單觀察中:
BOOL CALLBACK ProcessNM(LPSHNAMEMAPPING pshNM, DWORD dwData)
{
TCHAR szBuf[1024] = {0};
TCHAR szOldPath[MAX_PATH] = {0};
TCHAR szNewPath[MAX_PATH] = {0};
OSVERSIONINFO os;
// 我們需要知道在什麼樣的 OS 上
os.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx(&os);
BOOL bIsNT = (os.dwPlatformId == VER_PLATFORM_WIN32_NT);
// 在 NT 下,SHNAMEMAPPING結構包含 UNICODE 串
if(bIsNT)
{
WideCharToMultiByte(CP_ACP, 0, reinterpret_cast<LPWSTR>(pshNM->pszOldPath),
MAX_PATH, szOldPath, MAX_PATH, NULL, NULL);
WideCharToMultiByte(CP_ACP, 0, reinterpret_cast<LPWSTR>(pshNM->pszNewPath),
MAX_PATH, szNewPath, MAX_PATH, NULL, NULL);
}else{
lstrcpy(szOldPath, pshNM->pszOldPath);
lstrcpy(szNewPath, pshNM->pszNewPath);
}
// 儲存清單觀察Handle
HWND hwndListView = reinterpret_cast<HWND>(dwData);
// 建立 /0 分隔的串
LPTSTR psz = szBuf;
lstrcpyn(psz, szOldPath, pshNM->cchOldPath + 1);
lstrcat(psz, __TEXT("/0"));
psz += lstrlen(psz) + 1;
lstrcpyn(psz, szNewPath, pshNM->cchNewPath + 1);
lstrcat(psz, __TEXT("/0"));
// 加串到報告觀察中
LV_ITEM lvi;
ZeroMemory(&lvi, sizeof(LV_ITEM));
lvi.mask = LVIF_TEXT;
lvi.pszText = szBuf;
lvi.cchTextMax = lstrlen(szBuf);
lvi.iItem = 0;
ListView_InsertItem(hwndListView, &lvi);
psz = szBuf + lstrlen(szBuf) + 1;
ListView_SetItemText(hwndListView, 0, 1, psz);
return TRUE;
}
注意,在Windows NT下,SHNAMEMAPPING結構中的串是Unicode格式的。是以,如果作業系統是NT,則轉換串到ANSI格式,以便在例程中使用它們。還要注意的是dwData緩沖,它用于傳輸清單觀察的Handle到回調函數。
把這個代碼與較早期的例子內建到一起後,現在就能夠給出調用SHFileOperation()函數引起重命名檔案的詳細過程。在典型情況下測試,可以看到下面的情況:
小結
這一章深入讨論了一個函數SHFileOperation(),對它的每一個方面都作了徹底地測試了。從拷貝,移動,重命名或删除檔案,以及設定标志改變函數行為開始,然後展開了對某些未寫進資料的傳回碼,Bugs,函數缺陷的讨論。概括地講,在這一章中,給出了:
怎樣程式設計SHFileOperation()
最普遍的程式設計錯。
這個函數在資料方面的短缺
怎樣利用檔案名映射的優點
引用:http://blog.csdn.net/chchzh/article/details/2233634