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

圖二、運(yùn)行示例程序一后,使用 DFView.exe 打開觀察復(fù)合文件的效果圖
示例二:打開一個(gè)復(fù)合文件,枚舉其根存儲(chǔ)下的所有對(duì)象。
---(自己的理解,是不是每個(gè)對(duì)象就是一個(gè)子存儲(chǔ)??)
#i nclude六、小結(jié)// ANSI、MBCS、UNICODE 轉(zhuǎn)換 void SampleEnum() { // 假設(shè)你已經(jīng)做過 COM 初始化了 LPCTSTR lpFileName = _T( "c:\\a.stg" ); HRESULT hr; IStorage *pStg = NULL; USES_CONVERSION; // (注6) LPCOLESTR lpwFileName = T2COLE( lpFileName ); // 轉(zhuǎn)換T類型為寬字符 hr = ::StgIsStorageFile( lpwFileName ); // 是復(fù)合文件嗎? if( FAILED(hr) ) return; hr = ::StgOpenStorage( // 打開復(fù)合文件 lpwFileName, // 文件名稱 NULL, STGM_READ | STGM_SHARE_DENY_WRITE, 0, 0, &pStg); // 得到根存儲(chǔ)接口指針 IEnumSTATSTG *pEnum=NULL; // 枚舉器 hr = pStg->EnumElements( 0, NULL, 0, &pEnum ); ASSERT( SUCCEEDED(hr) ); STATSTG statstg; while( NOERROR == pEnum->Next( 1, &statstg, NULL) ) { // statstg.type 保存著對(duì)象類型 STGTY_STREAM 或 STGTY_STORAGE // statstg.pwcsName 保存著對(duì)象名稱 // ...... 還有時(shí)間,長(zhǎng)度等很多信息。請(qǐng)查看 MSDN ::CoTaskMemFree( statstg.pwcsName ); // 釋放名稱所使用的內(nèi)存(注6) } if( pEnum ) pEnum->Release(); if( pStg ) pStg->Release(); }
復(fù)合文件,結(jié)構(gòu)化存儲(chǔ),是微軟組件思想的起源,在此基礎(chǔ)上繼續(xù)發(fā)展出了持續(xù)性、命名、ActiveX、對(duì)象嵌入、現(xiàn)場(chǎng)激活......一系列的新技術(shù)、新概念。因此理解
留作業(yè)啦......
作業(yè)1:寫個(gè)小應(yīng)用程序,從 MSWORD 的 doc 文件中,提取出附加信息(作者、公司......)。
作業(yè)2:寫個(gè)全功能的“復(fù)合文件瀏覽編輯器”。
注1:踅摸(xuemo),動(dòng)詞,北方方言,尋找搜索的意思。
注2:?jiǎn)枺簽槭裁床簧暇W(wǎng)查資料學(xué)習(xí)?
???? 答:開什么國(guó)際玩笑!在那遙遠(yuǎn)的1995年代,我的500塊工資,不吃不喝正好夠上100小時(shí)的Internet網(wǎng)。
注3:OLE,對(duì)象的連接與嵌入。
注4:可以用 DFView.exe 打開 MSWORD 的 DOC 文件進(jìn)行復(fù)合文件的瀏覽。但是該程序并沒有實(shí)現(xiàn)國(guó)際化,不能打開中文文件名的復(fù)合文件,因此需要改名后才能瀏覽。
注5:CLSID,在后續(xù)的文章中介紹。
注6:關(guān)于 COM 中內(nèi)存使用的問題,在后續(xù)的文章中介紹。
posted on 2007-07-31 10:20 旅途 閱讀(626) 評(píng)論(0) 編輯 收藏 引用 所屬分類: COM+/DCOM