1、前言編程
公元一九九五年某個夜黑風高的晚上,個人一位老師跟我說:「小楊呀,之後寫程序就和搭積木同樣啦。你趕快學習一些OLE的技術吧......」,當時我內心就尋思 :「開什麼玩笑?搭積木方式寫程序?再過100年吧......」,但做爲一名聽話的好學生,我開始在書店裏「踅摸」(注1)有關OLE的書籍(注2)。功夫不負有心人,終於買到了個人第一本COM書《OLE2 高級編程技術》,這本800多頁的大布頭花費了我1/5的月工資呀......因而開始日夜耕讀.....瀏覽器
功夫不負有心人,我堅持讀完了所有著做,感想是:這本書,在說什麼吶?編輯器
功夫不負有心人,我又讀完了一遍大布頭,感想是:咳~~~,沒懂!函數
功夫不負有心人,我再,我再,我再讀 ... 感想是:哦~~~,讀懂了一點點啦,哈哈哈。工具
...... ......性能
功夫不負有心人,我終於,我終於懂了。學習
800頁的書對如今的我來講,其實也就10幾頁有用。到這時候才體會出什麼叫「書越讀越薄」的道理了。到後來,能買到的書也多了,上網也更方便更便宜了......spa
爲了讓VCKBASE上的朋友,再也不經歷我曾經的痛苦、再也不重蹈我「無頭蒼蠅」般探索的艱辛、爲了VCKBASE的蓬勃發展、爲了中國軟件事業的騰飛(糟糕,吹的太也高了)......我打算節約一些在 BBS 上賺分的時間,寫個系列論文,就叫「COM組件設計與應用」吧。今天是第一部分――起源。操作系統
2、文件的存儲線程
傳說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 的基石。下圖是磁盤文件組織方式與複合文件組織方式的類比圖:
圖1、左側表示一個磁盤下的文件組織方式,右側表示一個複合文件內部的數據組織方式。
3、複合文件的特色
1.複合文件的內部是使用指針構造的一棵樹進行管理的。編寫程序的時候要注意,因爲使用的是單向指針,所以當作定位操做的時候,向後定位比向前定位要快;
2.複合文件中的「流對象」,是真正保存數據的空間。它的存儲單位爲512字節。也就是說,即便你在流中只保存了一個字節的數據,它也要佔據512字節的文件空間。啊~~~,這也太浪費了呀?不浪費!由於文件保存在磁盤上,即便一個字節也還要佔用一個「簇」的空間那;
3.不一樣的進程,或同一個進程的不一樣線程能夠同時訪問一個複合文件的不一樣部分而互不干擾;
4.你們都有這樣的體會,當須要往一個文件中插入一個字節的話,須要對整個文件進行操做,很是煩瑣而且效率低下。而複合文件則提供了很是方便的「增量訪問」能力;
5.當頻繁地刪除文件,複製文件後,磁盤空間會變的很零碎,須要使用磁盤整理工具進行從新整合。和磁盤管理很是類似,複合文件也會產生這個問題,在適當的時候也須要整理,但比較簡單,只要調用一個函數就能夠完成了。
4、瀏覽複合文件
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。
下面程序片斷,演示了一些基本函數功能和調用方法。
示例一:創建一個複合文件,並在其下創建一個子存儲,在該子存儲中再創建一個流,寫入數據。
01.
void
SampleCreateDoc()
02.
{
03.
::CoInitialize(NULL);
// COM 初始化
04.
// 若是是MFC程序,可使用AfxOleInit()替代
05.
06.
HRESULT
hr;
// 函數執行返回值
07.
IStorage *pStg = NULL;
// 根存儲接口指針
08.
IStorage *pSub = NULL;
// 子存儲接口指針
09.
IStream *pStm = NULL;
// 流接口指針
10.
11.
hr = ::StgCreateDocfile(
// 創建複合文件
12.
L
"c:\\a.stg"
,
// 文件名稱
13.
STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,
// 打開方式
14.
0,
// 保留參數
15.
&pStg);
// 取得根存儲接口指針
16.
ASSERT( SUCCEEDED(hr) );
// 爲了突出重點,簡化程序結構,因此使用了斷言。
17.
// 在實際的程序中則要使用條件判斷和異常處理
18.
19.
hr = pStg->CreateStorage(
// 創建子存儲
20.
L
"SubStg"
,
// 子存儲名稱
21.
STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,
22.
0,0,
23.
&pSub);
// 取得子存儲接口指針
24.
ASSERT( SUCCEEDED(hr) );
25.
26.
hr = pSub->CreateStream(
// 創建流
27.
L
"Stm"
,
// 流名稱
28.
STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,
29.
0,0,
30.
&pStm);
// 取得流接口指針
31.
ASSERT( SUCCEEDED(hr) );
32.
33.
hr = pStm->Write(
// 向流中寫入數據
34.
"Hello"
,
// 數據地址
35.
5,
// 字節長度(注意,沒有寫入字符串結尾的\0)
36.
NULL);
// 不須要獲得實際寫入的字節長度
37.
ASSERT( SUCCEEDED(hr) );
38.
39.
if
( pStm ) pStm->Release();
// 釋放流指針
40.
if
( pSub ) pSub->Release();
// 釋放子存儲指針
41.
if
( pStg ) pStg->Release();
// 釋放根存儲指針
42.
43.
::CoUninitialize()
// COM 釋放
44.
// 若是使用 AfxOleInit(),則不調用該函數
45.
}
圖2、運行示例程序一後,使用 DFView.exe 打開觀察複合文件的效果圖
示例二:打開一個複合文件,枚舉其根存儲下的全部對象。
01.
#include < atlconv.h > // ANSI、MBCS、UNICODE 轉換
02.
03.
void
SampleEnum()
04.
{
// 假設你已經作過 COM 初始化了
05.
06.
LPCTSTR
lpFileName = _T(
"c:\\a.stg"
);
07.
HRESULT
hr;
08.
IStorage *pStg = NULL;
09.
10.
USES_CONVERSION;
// (注6)
11.
LPCOLESTR lpwFileName = T2COLE( lpFileName );
// 轉換T類型爲寬字符
12.
hr = ::StgIsStorageFile( lpwFileName );
// 是複合文件嗎?
13.
if
( FAILED(hr) )
return
;
14.
15.
hr = ::StgOpenStorage(
// 打開復合文件
16.
lpwFileName,
// 文件名稱
17.
NULL,
18.
STGM_READ | STGM_SHARE_DENY_WRITE,
19.
0,
20.
0,
21.
&pStg);
// 獲得根存儲接口指針
22.
23.
IEnumSTATSTG *pEnum=NULL;
// 枚舉器
24.
hr = pStg->EnumElements( 0, NULL, 0, &pEnum );
25.
ASSERT( SUCCEEDED(hr) );
26.
27.
STATSTG statstg;
28.
while
( NOERROR == pEnum->Next( 1, &statstg, NULL) )
29.
{
30.
// statstg.type 保存着對象類型 STGTY_STREAM 或 STGTY_STORAGE
31.
// statstg.pwcsName 保存着對象名稱
32.
// ...... 還有時間,長度等不少信息。請查看 MSDN
33.
34.
::CoTaskMemFree( statstg.pwcsName );
// 釋放名稱所使用的內存(注6)
35.
}
36.
37.
if
( pEnum ) pEnum->Release();
38.
if
( pStg ) pStg->Release();
39.
}
6、小結
複合文件,結構化存儲,是微軟組件思想的起源,在此基礎上繼續發展出了持續性、命名、ActiveX、對象嵌入、現場激活......一系列的新技術、新概念。所以理解和掌握 複合文件是很是重要的,即便在你的程序中並無全面使用組件技術,複合文件技術也是能夠單獨被應用的。祝你們學習快樂,爲社會主義軟件事業而奮鬥:-)
留做業啦......
做業1:寫個小應用程序,從 MSWORD 的 doc 文件中,提取出附加信息(做者、公司......)。
做業2:寫個全功能的「複合文件瀏覽編輯器」。
注1:踅摸(xuemo),動詞,北方方言,尋找搜索的意思。
注2:問:爲何不上網查資料學習?
答:開什麼國際玩笑!在那遙遠的1995年代,個人500塊工資,不吃不喝正好夠上100小時的Internet網。
注3:OLE,對象的鏈接與嵌入。
注4:能夠用 DFView.exe 打開 MSWORD 的 DOC 文件進行復合文件的瀏覽。可是該程序並無實現國際化,不能打開中文文件名的複合文件,所以須要更名後才能瀏覽。
注5:CLSID,在後續的文章中介紹。
注6:關於 COM 中內存使用的問題,在後續的文章中介紹。