一、前言
公元一九九五年某個(gè)夜黑風(fēng)高的晚上,我的一位老師跟我說(shuō):“小楊呀,以后寫(xiě)程序就和搭積木一樣啦。你趕快學(xué)習一些OLE的技術(shù)吧......”,當時(shí)我心里就尋思 :“開(kāi)什么玩笑?搭積木方式寫(xiě)程序?再過(guò)100年吧......”,但作為一名聽(tīng)話(huà)的好學(xué)生,我開(kāi)始在書(shū)店里“踅摸”(注1)有關(guān)OLE的書(shū)籍(注2)。功夫不負有心人,終于買(mǎi)到了我的第一本COM書(shū)《OLE2 高級編程技術(shù)》,這本800多頁(yè)的大布頭花費了我1/5的月工資呀......于是開(kāi)始日夜耕讀..... 功夫不負有心人,我堅持讀完了全部著(zhù)作,感想是:這本書(shū),在說(shuō)什么吶? 功夫不負有心人,我又讀完了一遍大布頭,感想是:咳~~~,沒(méi)懂! 功夫不負有心人,我再,我再,我再讀 ... 感想是:哦~~~,讀懂了一點(diǎn)點(diǎn)啦,哈哈哈。 ...... ...... 功夫不負有心人,我終于,我終于懂了。 800頁(yè)的書(shū)對現在的我來(lái)說(shuō),其實(shí)也就10幾頁(yè)有用。到這時(shí)候才體會(huì )出什么叫“書(shū)越讀越薄”的道理了。到后來(lái),能買(mǎi)到的書(shū)也多了,上網(wǎng)也更方便更便宜了......
為了讓VCKBASE上的朋友,不再經(jīng)歷我曾經(jīng)的痛苦、不再重蹈我“無(wú)頭蒼蠅”般探索的艱辛、為了VCKBASE的蓬勃發(fā)展、為了中國軟件事業(yè)的騰飛(糟糕,吹的太也高了)......我打算節約一些在 BBS 上賺分的時(shí)間,寫(xiě)個(gè)系列論文,就叫“COM組件設計與應用”吧。今天是第一部分——起源。
二、文件的存儲
傳說(shuō)350年前,牛頓被蘋(píng)果砸到了頭,于是發(fā)現了萬(wàn)有引力。但到了二十一世紀的現在,任何一個(gè)技術(shù)的發(fā)明和發(fā)展,已經(jīng)不再依靠圣人靈光的一閃。技術(shù)的進(jìn)步轉而是被社會(huì )的需求、商業(yè)的利益、競爭的壓力、行業(yè)的滲透等推動(dòng)的。微軟在Windows平臺上的組件技術(shù)也不例外,它的發(fā)明,有其必然因素。什么是這個(gè)因素那?答案是——文件的存儲。 打開(kāi)記事本程序,輸入了一篇文章后,保存?!@樣的文件叫“非結構化文件”; 打開(kāi)電子表格程序,輸入一個(gè)班的學(xué)生姓名和考試成績(jì),保存?!@樣的文件叫“標準結構化文件”; 在我們寫(xiě)的程序中,需要把特定的數據按照一定的結構和順序寫(xiě)到文件中保存?!@樣的文件叫“自定義結構化文件”;(比如 *.bmp 文件) 以上三種類(lèi)型的文件,大家都見(jiàn)的多了。那么文件存儲就依靠上述的方式能滿(mǎn)足所有的應用需求嗎?恩~~~,至少從計算機發(fā)明后的50多年來(lái),一直是夠用的了。嘿嘿,下面看看商業(yè)利益的推動(dòng)作用,對文件 的存儲形式產(chǎn)生了什么變化吧。30歲以上的朋友,我估計以前都使用過(guò)以下幾個(gè)著(zhù)名的軟件:WordStar(獨霸DOS下的英文編輯軟件),WPS(裘伯君寫(xiě)的中文編輯軟件,據說(shuō)當年的市場(chǎng)占有率高達90%,各種計算機培訓班的必修課程),LOTUS-123(蓮花公司出品的電子表格軟件)...... 微軟在成功地推出 Windows 3.1 后,開(kāi)始垂涎桌面辦公自動(dòng)化軟件領(lǐng)域。微軟的 OFFICE 開(kāi)發(fā)部門(mén),各小組分別獨立地開(kāi)發(fā)了 WORD 和 EXCEL 等軟件,并采用“自定義結構”方式,對文件進(jìn)行存儲。在激烈的市場(chǎng)競爭下,為了打敗競爭對手,微軟自然地產(chǎn)生了一個(gè)念頭------如果我能在 WORD 程序中嵌入 EXCEL,那么用戶(hù)在購買(mǎi)了我 WORD 軟件的情況下,不就沒(méi)有必要再買(mǎi) LOTUS-123 了嗎?!“惡毒”(中國微軟的同志們看到了這個(gè)詞,不要激動(dòng),我是加了引號的呀)的計劃產(chǎn)生后,他們開(kāi)始了實(shí)施工作,這就是 COM 的前身 OLE 的起源(注3)。但立刻就遇到了一個(gè)嚴重的技術(shù)問(wèn)題:需要把 WORD 產(chǎn)生的 DOC 文件和 EXCEL 產(chǎn)生的 XLS 文件保存在一起。 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) | | 建立一個(gè)子目錄,把 DOC、XLS 存儲在這同一個(gè)子目錄中。 | 數據隔離性好,WORD 不用了解 EXCEL 的存儲結構;容易擴展。 | 結構太松散,容易造成數據的損壞或丟失。 不易攜帶。 | | 修改文件存儲結構,在DOC結構基礎上擴展出包容 XLS 的結構。 | 結構緊密,容易攜帶和統一管理。 | WORD 的開(kāi)發(fā)人員需要通曉 EXCEL 的存儲格式;缺少擴展性,總不能新加一個(gè)類(lèi)型就擴展一下結構吧?! |
以上兩個(gè)方案,都有嚴重的缺陷,怎么解決那?如果能有一個(gè)新方案,能夠合并前兩個(gè)方案的優(yōu)點(diǎn),消滅缺點(diǎn),該多好呀......微軟是作磁盤(pán)操作系統起家的,于是很自然地他們提出了一個(gè)非常完美的設計方案,那就是把磁盤(pán)文件的管理方式移植到文件中了------復合文件,俗稱(chēng)“文件中的文件系統”。連微軟當年都沒(méi)有想到,就這么一個(gè)簡(jiǎn)單的想法,居然最后就演變出了 COM 組件程序設計的方法??梢哉f(shuō),復合文件是 COM 的基石。下圖是磁盤(pán)文件組織方式與復合文件組織方式的類(lèi)比圖:
圖一、左側表示一個(gè)磁盤(pán)下的文件組織方式,右側表示一個(gè)復合文件內部的數據組織方式。
三、復合文件的特點(diǎn) - 復合文件的內部是使用指針構造的一棵樹(shù)進(jìn)行管理的。編寫(xiě)程序的時(shí)候要注意,由于使用的是單向指針,因此當做定位操作的時(shí)候,向后定位比向前定位要快;
- 復合文件中的“流對象”,是真正保存數據的空間。它的存儲單位為512字節。也就是說(shuō),即使你在流中只保存了一個(gè)字節的數據,它也要占據512字節的文件空間。啊~~~,這也太浪費了呀?不浪費!因為文件保存在磁盤(pán)上,即使一個(gè)字節也還要占用一個(gè)“簇”的空間那;
- 不同的進(jìn)程,或同一個(gè)進(jìn)程的不同線(xiàn)程可以同時(shí)訪(fǎng)問(wèn)一個(gè)復合文件的不同部分而互不干擾;
- 大家都有這樣的體會(huì ),當需要往一個(gè)文件中插入一個(gè)字節的話(huà),需要對整個(gè)文件進(jìn)行操作,非常煩瑣并且效率低下。而復合文件則提供了非常方便的“增量訪(fǎng)問(wèn)”能力;
- 當頻繁地刪除文件,復制文件后,磁盤(pán)空間會(huì )變的很零碎,需要使用磁盤(pán)整理工具進(jìn)行重新整合。和磁盤(pán)管理非常相似,復合文件也會(huì )產(chǎn)生這個(gè)問(wèn)題,在適當的時(shí)候也需要整理,但比較簡(jiǎn)單,只要調用一個(gè)函數就可以完成了。
四、瀏覽復合文件
VC6.0 附帶了一個(gè)工具軟件“復合文件瀏覽器”,文件名是“vc目錄\Common\Tools\DFView.exe”。為了方便使用該程序,可以把它加到工具(tools)菜單中。方法是:Tools\Customize...\Tools卡片中增加新的項目。運行 DFView.exe,就可以打開(kāi)一個(gè)復合文件進(jìn)行觀(guān)察了(注4)。但奇怪的是,在 Microsoft Visual Studio .NET 2003 中,我反而找不到這個(gè)工具程序了,汗!不過(guò)這恰好提供給大家一個(gè)練習的機會(huì ),在你閱讀完本篇文章并掌握了編程方法后,自己寫(xiě)一個(gè)“復合文件瀏覽編輯器”程序,又練手了,還有實(shí)用的價(jià)值。
五、復合文件函數
復合文件的函數和磁盤(pán)目錄文件的操作非常類(lèi)似。所有這些函數,被分為3種類(lèi)型:WIN API 全局函數,存儲 IStorage 接口函數,流 IStream 接口函數。什么是接口?什么是接口函數?以后的文章中再陸續介紹,這里大家只要把“接口”看成是完成一組相關(guān)操作功能的函數集合就可以了。 WIN API 函數 | 功能說(shuō)明 | | StgCreateDocfile() | 建立一個(gè)復合文件,得到根存儲對象 | | StgOpenStorage() | 打開(kāi)一個(gè)復合文件,得到根存儲對象 | | StgIsStorageFile() | 判斷一個(gè)文件是否是復合文件 | | IStorage 函數 | 功能說(shuō)明 | | CreateStorage() | 在當前存儲中建立新存儲,得到子存儲對象 | | CreateStream() | 在當前存儲中建立新流,得到流對象 | | OpenStorage() | 打開(kāi)子存儲,得到子存儲對象 | | OpenStream() | 打開(kāi)流,得到流對象 | | CopyTo() | 復制存儲下的所有對象到目標存儲中,該函數可以實(shí)現“整理文件,釋放碎片空間”的功能 | | MoveElementTo() | 移動(dòng)對象到目標存儲中 | | DestoryElement() | 刪除對象 | | RenameElement() | 重命名對象 | | EnumElements() | 枚舉當前存儲中所有的對象 | | SetElementTimes() | 修改對象的時(shí)間 | | SetClass() | 在當前存儲中建立一個(gè)特殊的流對象,用來(lái)保存CLSID(注5) | | Stat() | 取得當前存儲中的系統信息 | | Release() | 關(guān)閉存儲對象 | | | IStream 函數 | 功能說(shuō)明 | | Read() | 從流中讀取數據 | | Write() | 向流中寫(xiě)入數據 | | Seek() | 定位讀寫(xiě)位置 | | SetSize() | 設置流尺寸。如果預先知道大小,那么先調用這個(gè)函數,可以提高性能 | | CopyTo() | 復制流數據到另一個(gè)流對象中 | | Stat() | 取得當前流中的系統信息 | | Clone() | 克隆一個(gè)流對象,方便程序中的不同模塊操作同一個(gè)流對象 | | Release() | 關(guān)閉流對象 | | | | WIN API 補充函數 | 功能說(shuō)明 | | WriteClassStg() | 寫(xiě)CLSID到存儲中,同IStorage::SetClass() | | ReadClassStg() | 讀出WriteClassStg()寫(xiě)入的CLSID,相當于簡(jiǎn)化調用IStorage::Stat() | | WriteClassStm() | 寫(xiě)CLSID到流的開(kāi)始位置 | | ReadClassStm() | 讀出WriteClassStm()寫(xiě)入的CLSID | | WriteFmtUserTypeStg() | 寫(xiě)入用戶(hù)指定的剪貼板格式和名稱(chēng)到存儲中 | | ReadFmtUserTypeStg() | 讀出WriteFmtUserTypeStg()寫(xiě)入的信息。方便應用程序快速判斷是否是它需要的格式數據。 | | CreateStreamOnHGlobal() | 內存句柄 HGLOBAL 轉換為流對象 | | GetHGlobalFromStream() | 取得CreateStreamOnHGlobal()調用中使用的內存句柄 |
為了讓大家快速地瀏覽和掌握基本方法,上面所列表的函數并不是全部,我省略了“事務(wù)”函數和未實(shí)現函數部分。更全面的介紹,請閱讀 MSDN。 下面程序片段,演示了一些基本函數功能和調用方法。 示例一:建立一個(gè)復合文件,并在其下建立一個(gè)子存儲,在該子存儲中再建立一個(gè)流,寫(xiě)入數據。 void SampleCreateDoc(){ ::CoInitialize(NULL); // COM 初始化 // 如果是MFC程序,可以使用AfxOleInit()替代 HRESULT hr; // 函數執行返回值 IStorage *pStg = NULL; // 根存儲接口指針 IStorage *pSub = NULL; // 子存儲接口指針 IStream *pStm = NULL; // 流接口指針 hr = ::StgCreateDocfile( // 建立復合文件 L"c:\\a.stg", // 文件名稱(chēng) STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE, // 打開(kāi)方式 0, // 保留參數 &pStg); // 取得根存儲接口指針 ASSERT( SUCCEEDED(hr) ); // 為了突出重點(diǎn),簡(jiǎn)化程序結構,所以使用了斷言。 // 在實(shí)際的程序中則要使用條件判斷和異常處理 hr = pStg->CreateStorage( // 建立子存儲 L"SubStg", // 子存儲名稱(chēng) STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE, 0,0, &pSub); // 取得子存儲接口指針 ASSERT( SUCCEEDED(hr) ); hr = pSub->CreateStream( // 建立流 L"Stm", // 流名稱(chēng) STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE, 0,0, &pStm); // 取得流接口指針 ASSERT( SUCCEEDED(hr) ); hr = pStm->Write( // 向流中寫(xiě)入數據 "Hello", // 數據地址 5, // 字節長(cháng)度(注意,沒(méi)有寫(xiě)入字符串結尾的\0) NULL); // 不需要得到實(shí)際寫(xiě)入的字節長(cháng)度 ASSERT( SUCCEEDED(hr) ); if( pStm ) pStm->Release();// 釋放流指針 if( pSub ) pSub->Release();// 釋放子存儲指針 if( pStg ) pStg->Release();// 釋放根存儲指針 ::CoUninitialize() // COM 釋放 // 如果使用 AfxOleInit(),則不調用該函數} 圖二、運行示例程序一后,使用 DFView.exe 打開(kāi)觀(guān)察復合文件的效果圖
示例二:打開(kāi)一個(gè)復合文件,枚舉其根存儲下的所有對象。#include <atlconv.h> // ANSI、MBCS、UNICODE 轉換void SampleEnum() { // 假設你已經(jīng)做過(guò) COM 初始化了 LPCTSTR lpFileName = _T( "c:\\a.stg" ); HRESULT hr; IStorage *pStg = NULL; USES_CONVERSION; // (注6) LPCOLESTR lpwFileName = T2COLE( lpFileName ); // 轉換T類(lèi)型為寬字符 hr = ::StgIsStorageFile( lpwFileName ); // 是復合文件嗎? if( FAILED(hr) ) return; hr = ::StgOpenStorage( // 打開(kāi)復合文件 lpwFileName, // 文件名稱(chēng) NULL, STGM_READ | STGM_SHARE_DENY_WRITE, 0, 0, &pStg); // 得到根存儲接口指針 IEnumSTATSTG *pEnum=NULL; // 枚舉器 hr = pStg->EnumElements( 0, NULL, 0, &pEnum ); ASSERT( SUCCEEDED(hr) ); STATSTG statstg; while( NOERROR == pEnum->Next( 1, &statstg, NULL) ) { // statstg.type 保存著(zhù)對象類(lèi)型 STGTY_STREAM 或 STGTY_STORAGE // statstg.pwcsName 保存著(zhù)對象名稱(chēng) // ...... 還有時(shí)間,長(cháng)度等很多信息。請查看 MSDN ::CoTaskMemFree( statstg.pwcsName ); // 釋放名稱(chēng)所使用的內存(注6) } if( pEnum ) pEnum->Release(); if( pStg ) pStg->Release();}六、小結
復合文件,結構化存儲,是微軟組件思想的起源,在此基礎上繼續發(fā)展出了持續性、命名、ActiveX、對象嵌入、現場(chǎng)激活......一系列的新技術(shù)、新概念。因此理解和掌握 復合文件是非常重要的,即使在你的程序中并沒(méi)有全面使用組件技術(shù),復合文件技術(shù)也是可以單獨被應用的。祝大家學(xué)習快樂(lè ),為社會(huì )主義軟件事業(yè)而奮斗:-)
留作業(yè)啦...... 作業(yè)1:寫(xiě)個(gè)小應用程序,從 MSWORD 的 doc 文件中,提取出附加信息(作者、公司......)。 作業(yè)2:寫(xiě)個(gè)全功能的“復合文件瀏覽編輯器”。
注1:踅摸(xuemo),動(dòng)詞,北方方言,尋找搜索的意思。 注2:?jiǎn)?wèn):為什么不上網(wǎng)查資料學(xué)習? 答:開(kāi)什么國際玩笑!在那遙遠的1995年代,我的500塊工資,不吃不喝正好夠上100小時(shí)的Internet網(wǎng)。 注3:OLE,對象的連接與嵌入。 注4:可以用 DFView.exe 打開(kāi) MSWORD 的 DOC 文件進(jìn)行復合文件的瀏覽。但是該程序并沒(méi)有實(shí)現國際化,不能打開(kāi)中文文件名的復合文件,因此需要改名后才能瀏覽。 注5:CLSID,在后續的文章中介紹。 注6:關(guān)于 COM 中內存使用的問(wèn)題,在后續的文章中介紹。 |