一、前言
公元一九九五年某個夜黑風高的晚上,我的一位老師跟我說:“小楊呀,以後寫程式就和搭積木一樣啦。你趕
快學習一些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() |
設定流尺寸。如果預Crowdsourced Security Testing道大小,那麼先調用這個函數,可以提高效能 |
| 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 開啟觀察複合檔案的
樣本二:開啟一個複合檔案,枚舉其根儲存下的所有對象。
#include <atlconv.h>// 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 中記憶體使用量的問題,在後續的文章中介紹。
原文地址
http://www.vckbase.com/document/viewdoc/?id=1483