一、前言
公元一九九五年某個夜黑風高的晚上,我的一位老師跟我說:“小楊呀,以后寫程序就和搭積木一樣啦。你趕快學習一些OLE的技術吧......”,當時我心里就尋思 :“開什么玩笑?搭積木方式寫程序?再過100年吧......”,但作為一名聽話的好學生,我開始在書店里“踅摸”(注1)有關OLE的書籍(注2)。功夫不負有心人,終于買到了我的第一本COM書《OLE2 高級編程技術》,這本800多頁的大布頭花費了我1/5的月工資呀......于是開始日夜耕讀.....
??? 功夫不負有心人,我堅持讀完了全部著作,感想是:這本書,在說什么吶?
??? 功夫不負有心人,我又讀完了一遍大布頭,感想是:咳~~~,沒懂!
??? 功夫不負有心人,我再,我再,我再讀 ... 感想是:哦~~~,讀懂了一點點啦,哈哈哈。
??? ......? ......
??? 功夫不負有心人,我終于,我終于懂了。
??? 800頁的書對現在的我來說,其實也就10幾頁有用。到這時候才體會出什么叫“書越讀越薄”的道理了。到后來,能買到的書也多了,上網也更方便更便宜了......
為了讓VCKBASE上的朋友,不再經歷我曾經的痛苦、不再重蹈我“無頭蒼蠅”般探索的艱辛、為了VCKBASE的蓬勃發展、為了中國軟件事業的騰飛(糟糕,吹的太也高了)......我打算節約一些在 BBS 上賺分的時間,寫個系列論文,就叫“COM組件設計與應用”吧。今天是第一部分——起源。
二、文件的存儲
傳說350年前,牛頓被蘋果砸到了頭,于是發現了萬有引力。但到了二十一世紀的現在,任何一個技術的發明和發展,已經不再依靠圣人靈光的一閃。技術的進步轉而是被社會的需求、商業的利益、競爭的壓力、行業的滲透等推動的。微軟在Windows平臺上的組件技術也不例外,它的發明,有其必然因素。什么是這個因素那?答案是——文件的存儲。
打開記事本程序,輸入了一篇文章后,保存。——這樣的文件叫“非結構化文件”;
打開電子表格程序,輸入一個班的學生姓名和考試成績,保存。——這樣的文件叫“標準結構化文件”;
在我們寫的程序中,需要把特定的數據按照一定的結構和順序寫到文件中保存。——這樣的文件叫“自定義結構化文件”;(比如 *.bmp 文件)
以上三種類型的文件,大家都見的多了。那么文件存儲就依靠上述的方式能滿足所有的應用需求嗎?恩~~~,至少從計算機發明后的50多年來,一直是夠用的了。嘿嘿,下面看看商業利益的推動作用,對文件 的存儲形式產生了什么變化吧。30歲以上的朋友,我估計以前都使用過以下幾個著名的軟件:WordStar(獨霸DOS下的英文編輯軟件),WPS(裘伯君寫的中文編輯軟件,據說當年的市場占有率高達90%,各種計算機培訓班的必修課程),LOTUS-123(蓮花公司出品的電子表格軟件)......
??? 微軟在成功地推出 Windows 3.1 后,開始垂涎桌面辦公自動化軟件領域。微軟的 OFFICE 開發部門,各小組分別獨立地開發了 WORD 和 EXCEL 等軟件,并采用“自定義結構”方式,對文件進行存儲。在激烈的市場競爭下,為了打敗競爭對手,微軟自然地產生了一個念頭------如果我能在 WORD 程序中嵌入 EXCEL,那么用戶在購買了我 WORD 軟件的情況下,不就沒有必要再買 LOTUS-123 了嗎?!“惡毒”(中國微軟的同志們看到了這個詞,不要激動,我是加了引號的呀)的計劃產生后,他們開始了實施工作,這就是 COM 的前身 OLE 的起源(注3)。但立刻就遇到了一個嚴重的技術問題:需要把 WORD 產生的 DOC 文件和 EXCEL 產生的 XLS 文件保存在一起。
方案
|
優點
|
缺點
|
建立一個子目錄,把 DOC、XLS 存儲在這同一個子目錄中。 |
數據隔離性好,WORD 不用了解 EXCEL 的存儲結構;容易擴展。 |
結構太松散,容易造成數據的損壞或丟失。 不易攜帶。 |
修改文件存儲結構,在DOC結構基礎上擴展出包容 XLS 的結構。 |
結構緊密,容易攜帶和統一管理。 |
WORD 的開發人員需要通曉 EXCEL 的存儲格式;缺少擴展性,總不能新加一個類型就擴展一下結構吧?! |
??? 以上兩個方案,都有嚴重的缺陷,怎么解決那?如果能有一個新方案,能夠合并前兩個方案的優點,消滅缺點,該多好呀......微軟是作磁盤***作系統起家的,于是很自然地他們提出了一個非常完美的設計方案,那就是把磁盤文件的管理方式移植到文件中了------復合文件,俗稱“文件中的文件系統”。連微軟當年都沒有想到,就這么一個簡單的想法,居然最后就演變出了 COM 組件程序設計的方法。可以說,復合文件是 COM 的基石。下圖是磁盤文件組織方式與復合文件組織方式的類比圖:

圖一、左側表示一個磁盤下的文件組織方式,右側表示一個復合文件內部的數據組織方式。
三、復合文件的特點
- 復合文件的內部是使用指針構造的一棵樹進行管理的。編寫程序的時候要注意,由于使用的是單向指針,因此當做定位***作的時候,向后定位比向前定位要快;
- 復合文件中的“流對象”,是真正保存數據的空間。它的存儲單位為512字節。也就是說,即使你在流中只保存了一個字節的數據,它也要占據512字節的文件空間。啊~~~,這也太浪費了呀?不浪費!因為文件保存在磁盤上,即使一個字節也還要占用一個“簇”的空間那;
- 不同的進程,或同一個進程的不同線程可以同時訪問一個復合文件的不同部分而互不干擾;
- 大家都有這樣的體會,當需要往一個文件中插入一個字節的話,需要對整個文件進行***作,非常煩瑣并且效率低下。而復合文件則提供了非常方便的“增量訪問”能力;
- 當頻繁地刪除文件,復制文件后,磁盤空間會變的很零碎,需要使用磁盤整理工具進行重新整合。和磁盤管理非常相似,復合文件也會產生這個問題,在適當的時候也需要整理,但比較簡單,只要調用一個函數就可以完成了。
四、瀏覽復合文件
VC6.0 附帶了一個工具軟件“復合文件瀏覽器”,文件名是“vc目錄\Common\Tools\DFView.exe”。為了方便使用該程序,可以把它加到工具(tools)菜單中。方法是:Tools\Customize...\Tools卡片中增加新的項目。運行 DFView.exe,就可以打開一個復合文件進行觀察了(注4)。但奇怪的是,在 Microsoft Visual Studio .NET 2003 中,我反而找不到這個工具程序了,汗!不過這恰好提供給大家一個練習的機會,在你閱讀完本篇文章并掌握了編程方法后,自己寫一個“復合文件瀏覽編輯器”程序,又練手了,還有實用的價值。
五、復合文件函數
復合文件的函數和磁盤目錄文件的***作非常類似。所有這些函數,被分為3種類型:WIN API 全局函數,存儲 IStorage 接口函數,流 IStream 接口函數。什么是接口?什么是接口函數?以后的文章中再陸續介紹,這里大家只要把“接口”看成是完成一組相關***作功能的函數集合就可以了。
WIN API 函數
|
功能說明
|
StgCreateDocfile() |
建立一個復合文件,得到根存儲對象 |
StgOpenStorage() |
打開一個復合文件,得到根存儲對象 |
StgIsStorageFile() |
判斷一個文件是否是復合文件 |
|
IStorage 函數
|
功能說明
|
CreateStorage() |
在當前存儲中建立新存儲,得到子存儲對象 |
CreateStream() |
在當前存儲中建立新流,得到流對象 |
OpenStorage() |
打開子存儲,得到子存儲對象 |
OpenStream() |
打開流,得到流對象 |
CopyTo() |
復制存儲下的所有對象到目標存儲中,該函數可以實現“整理文件,釋放碎片空間”的功能 |
MoveElementTo() |
移動對象到目標存儲中 |
DestoryElement() |
刪除對象 |
RenameElement() |
重命名對象 |
EnumElements() |
枚舉當前存儲中所有的對象 |
SetElementTimes() |
修改對象的時間 |
SetClass() |
在當前存儲中建立一個特殊的流對象,用來保存CLSID(注5) |
Stat() |
取得當前存儲中的系統信息 |
Release() |
關閉存儲對象 |
|
IStream 函數
|
功能說明
|
Read() |
從流中讀取數據 |
Write() |
向流中寫入數據 |
Seek() |
定位讀寫位置 |
SetSize() |
設置流尺寸。如果預先知道大小,那么先調用這個函數,可以提高性能 |
CopyTo() |
復制流數據到另一個流對象中 |
Stat() |
取得當前流中的系統信息 |
Clone() |
克隆一個流對象,方便程序中的不同模塊***作同一個流對象 |
Release() |
關閉流對象 |
|
WIN API 補充函數
|
功能說明
|
WriteClassStg() |
寫CLSID到存儲中,同IStorage::SetClass() |
ReadClassStg() |
讀出WriteClassStg()寫入的CLSID,相當于簡化調用IStorage::Stat() |
WriteClassStm() |
寫CLSID到流的開始位置 |
ReadClassStm() |
讀出WriteClassStm()寫入的CLSID |
WriteFmtUserTypeStg() |
寫入用戶指定的剪貼板格式和名稱到存儲中 |
ReadFmtUserTypeStg() |
讀出WriteFmtUserTypeStg()寫入的信息。方便應用程序快速判斷是否是它需要的格式數據。 |
CreateStreamOnHGlobal() |
內存句柄 HGLOBAL 轉換為流對象 |
GetHGlobalFromStream() |
取得CreateStreamOnHGlobal()調用中使用的內存句柄 |
為了讓大家快速地瀏覽和掌握基本方法,上面所列表的函數并不是全部,我省略了“事務”函數和未實現函數部分。更全面的介紹,請閱讀 MSDN。
??? 下面程序片段,演示了一些基本函數功能和調用方法。?
示例一:建立一個復合文件,并在其下建立一個子存儲,在該子存儲中再建立一個流,寫入數據。
void SampleCreateDoc()
{
::CoInitialize(NULL); // COM 初始化
// 如果是MFC程序,可以使用AfxOleInit()替代
HRESULT hr; // 函數執行返回值
IStorage *pStg = NULL; // 根存儲接口指針
IStorage *pSub = NULL; // 子存儲接口指針
IStream *pStm = NULL; // 流接口指針
hr = ::StgCreateDocfile( // 建立復合文件
L"c:\\a.stg", // 文件名稱
STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE, // 打開方式
0, // 保留參數
&pStg); // 取得根存儲接口指針
ASSERT( SUCCEEDED(hr) ); // 為了突出重點,簡化程序結構,所以使用了斷言。
// 在實際的程序中則要使用條件判斷和異常處理
hr = pStg->CreateStorage( // 建立子存儲
L"SubStg", // 子存儲名稱
STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,
0,0,
&pSub); // 取得子存儲接口指針
ASSERT( SUCCEEDED(hr) );
hr = pSub->CreateStream( // 建立流
L"Stm", // 流名稱
STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,
0,0,
&pStm); // 取得流接口指針
ASSERT( SUCCEEDED(hr) );
hr = pStm->Write( // 向流中寫入數據
"Hello", // 數據地址
5, // 字節長度(注意,沒有寫入字符串結尾的\0)
NULL); // 不需要得到實際寫入的字節長度
ASSERT( SUCCEEDED(hr) );
if( pStm ) pStm->Release();// 釋放流指針
if( pSub ) pSub->Release();// 釋放子存儲指針
if( pStg ) pStg->Release();// 釋放根存儲指針
::CoUninitialize() // COM 釋放
// 如果使用 AfxOleInit(),則不調用該函數
}
圖二、運行示例程序一后,使用 DFView.exe 打開觀察復合文件的效果圖
示例二:打開一個復合文件,
枚舉其根存儲下的所有對象。
---(
自己的理解,是不是每個對象就是一個子存儲??)#i nclude // ANSI、MBCS、UNICODE 轉換
void SampleEnum()
{ // 假設你已經做過 COM 初始化了
LPCTSTR lpFileName = _T( "c:\\a.stg" );
HRESULT hr;
IStorage *pStg = NULL;
USES_CONVERSION; // (注6)
LPCOLESTR lpwFileName = T2COLE( lpFileName ); // 轉換T類型為寬字符
hr = ::StgIsStorageFile( lpwFileName ); // 是復合文件嗎?
if( FAILED(hr) ) return;
hr = ::StgOpenStorage( // 打開復合文件
lpwFileName, // 文件名稱
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 保存著對象類型 STGTY_STREAM 或 STGTY_STORAGE
// statstg.pwcsName 保存著對象名稱
// ...... 還有時間,長度等很多信息。請查看 MSDN
::CoTaskMemFree( statstg.pwcsName ); // 釋放名稱所使用的內存(注6)
}
if( pEnum ) pEnum->Release();
if( pStg ) pStg->Release();
}
六、小結 復合文件,結構化存儲,是微軟組件思想的起源,在此基礎上繼續發展出了持續性、命名、ActiveX、對象嵌入、現場激活......一系列的新技術、新概念。因此理解
和掌握 復合文件是非常重要的,即使在你的程序中并沒有全面使用組件技術,復合文件技術也是可以單獨被應用的。祝大家學習快樂,為社會主義軟件事業而奮斗:-)
留作業啦......
作業1:寫個小應用程序,從 MSWORD 的 doc 文件中,提取出附加信息(作者、公司......)。
作業2:寫個全功能的“復合文件瀏覽編輯器”。
注1:踅摸(xuemo),動詞,北方方言,尋找搜索的意思。
注2:問:為什么不上網查資料學習?
???? 答:開什么國際玩笑!在那遙遠的1995年代,我的500塊工資,不吃不喝正好夠上100小時的Internet網。
注3:OLE,對象的連接與嵌入。
注4:可以用 DFView.exe 打開 MSWORD 的 DOC 文件進行復合文件的瀏覽。但是該程序并沒有實現國際化,不能打開中文文件名的復合文件,因此需要改名后才能瀏覽。
注5:CLSID,在后續的文章中介紹。
注6:關于 COM 中內存使用的問題,在后續的文章中介紹。