一般在XP檔案夾裡面,特别是圖檔和視訊檔案夾裡有一個檔案—Thumbs.db檔案。這個檔案是XP用來緩存圖檔和影音檔案的縮略圖的,有了這個檔案,XP在打開儲存大量圖檔檔案的檔案夾的時候,顯示速度會明顯比沒有Thumbs.db檔案的檔案夾快—因為後者需要實時生成縮略圖。
最近在做一個自己的圖檔管理程式,需要快速生成縮略圖,就想到複用這個檔案,這樣我的程式可以無縫地繼承視窗系統的資料總管功能。因為Thumbs.db檔案的檔案結構和通路API沒有被公開,是以在Google查了一些資料,發現Thumbs.db檔案采用的是結構化存儲檔案(Structured Storage File)結構,這個檔案在COM時代非常的流行,不知道為什麼在.Net裡面,微軟把這個檔案結構扔掉了。
結構化存儲概述
結構化存儲檔案結構說白了就是一個儲存在檔案裡面的檔案系統,就是說在一個結構化存儲檔案裡面,儲存有“檔案夾”資訊,也儲存有“檔案”資訊和其内容。例如,我們熟悉的Winrar的打包多個檔案的過程,就可以使用結構化存儲檔案結構來儲存(當然啦,我沒有Winrar的源代碼,不是說Winrar就是這樣實作打包的啊)。
使用結構化存儲檔案的一個好處是,使得更新檔案内容非常友善。 舉個例子,比如我們日常使用的Word吧,當我們編輯一個檔案的時候,如果Word采用的順序存儲結構—檔案内容是按照内容的邏輯結構順序存儲在磁盤裡的,即在硬碟裡,第一頁儲存在第二頁的前面。順序存儲方式的問題在于,它使得修改Word文檔的時候,會變得非常麻煩。假設你的文檔有幾千頁,當你增删第一頁的内容的時候,順序存儲的方式就要求你必須移動後面幾千頁内容—可以想象到這個過程有多慢了。 如果我們将Word文檔看作一個小的檔案系統的話,那麼對于文檔中的每一頁我們可以看成是一個“檔案夾”,然後所有的文字段落可以看成是“檔案夾”裡面的檔案。如果文檔裡面插入了圖檔的話,可以另外在“檔案夾”裡建立一個小的檔案夾—“圖檔”檔案夾,而在使用到這個圖檔的位置上加入一個快捷方式連結到每一頁的内容裡就可以了。下圖示範了前一段描述的概念(注意-我沒有看到Office的源代碼,上述内容隻不過是我的一個小猜想而已):
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsATOfd3bkFGazxCMx8VesATMfhHLlN3XnxCMwEzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5yYmNjNmRDZ3kDOzIzM4MGNjJjY2EmZ2EjYjVTYxUmZz8CX1AzLchDMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLzM3Lc9CX6MHc0RHaiojIsJye.png)
結構化存儲檔案的COM接口
剛才講完了概念,在COM中,IStorage接口就相當于結構化存儲檔案中的 “檔案夾”,而IStream接口就是“檔案”啦。下面就是IStorage的接口:
MIDL_INTERFACE("0000000b-0000-0000-C000-000000000046") IStorage : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE CreateStream( /* [string][in] */ __RPC__in const OLECHAR *pwcsName, /* [in] */ DWORD grfMode, /* [in] */ DWORD reserved1, /* [in] */ DWORD reserved2, /* [out] */ __RPC__deref_out_opt IStream **ppstm) = 0; virtual /* [local] */ HRESULT STDMETHODCALLTYPE OpenStream( /* [string][in] */ const OLECHAR *pwcsName, /* [unique][in] */ void *reserved1, /* [out] */ IStream **ppstm) = 0; virtual HRESULT STDMETHODCALLTYPE CreateStorage( /* [out] */ __RPC__deref_out_opt IStorage **ppstg) = 0; virtual HRESULT STDMETHODCALLTYPE OpenStorage( /* [string][unique][in] */ __RPC__in_opt const OLECHAR *pwcsName, /* [unique][in] */ __RPC__in_opt IStorage *pstgPriority, /* [unique][in] */ __RPC__deref_opt_in_opt SNB snbExclude, /* [in] */ DWORD reserved, virtual /* [local] */ HRESULT STDMETHODCALLTYPE CopyTo( /* [in] */ DWORD ciidExclude, /* [size_is][unique][in] */ const IID *rgiidExclude, /* [unique][in] */ SNB snbExclude, /* [unique][in] */ IStorage *pstgDest) = 0; virtual HRESULT STDMETHODCALLTYPE MoveElementTo( /* [unique][in] */ __RPC__in_opt IStorage *pstgDest, /* [string][in] */ __RPC__in const OLECHAR *pwcsNewName, /* [in] */ DWORD grfFlags) = 0; virtual HRESULT STDMETHODCALLTYPE Commit( /* [in] */ DWORD grfCommitFlags) = 0; virtual HRESULT STDMETHODCALLTYPE Revert( void) = 0; virtual /* [local] */ HRESULT STDMETHODCALLTYPE EnumElements( /* [size_is][unique][in] */ void *reserved2, /* [in] */ DWORD reserved3, /* [out] */ IEnumSTATSTG **ppenum) = 0; virtual HRESULT STDMETHODCALLTYPE DestroyElement( /* [string][in] */ __RPC__in const OLECHAR *pwcsName) = 0; virtual HRESULT STDMETHODCALLTYPE RenameElement( /* [string][in] */ __RPC__in const OLECHAR *pwcsOldName, /* [string][in] */ __RPC__in const OLECHAR *pwcsNewName) = 0; virtual HRESULT STDMETHODCALLTYPE SetElementTimes( /* [unique][in] */ __RPC__in_opt const FILETIME *pctime, /* [unique][in] */ __RPC__in_opt const FILETIME *patime, /* [unique][in] */ __RPC__in_opt const FILETIME *pmtime) = 0; virtual HRESULT STDMETHODCALLTYPE SetClass( /* [in] */ __RPC__in REFCLSID clsid) = 0; virtual HRESULT STDMETHODCALLTYPE SetStateBits( /* [in] */ DWORD grfStateBits, /* [in] */ DWORD grfMask) = 0; virtual HRESULT STDMETHODCALLTYPE Stat( /* [out] */ __RPC__out STATSTG *pstatstg, /* [in] */ DWORD grfStatFlag) = 0; }; |
注意上面的定義裡面,[Create/Open]Stream就是建立和打開“檔案”的方式,而 [Create/Open]Storage就是建立和打開“檔案夾”的方式—“檔案夾”裡面不是可以包含其他的檔案夾嗎?下面是IStream接口的定義:
MIDL_INTERFACE("0000000c-0000-0000-C000-000000000046") IStream : public ISequentialStream { public: virtual /* [local] */ HRESULT STDMETHODCALLTYPE Seek( /* [in] */ LARGE_INTEGER dlibMove, /* [in] */ DWORD dwOrigin, /* [out] */ ULARGE_INTEGER *plibNewPosition) = 0; virtual HRESULT STDMETHODCALLTYPE SetSize( /* [in] */ ULARGE_INTEGER libNewSize) = 0; virtual /* [local] */ HRESULT STDMETHODCALLTYPE CopyTo( /* [unique][in] */ IStream *pstm, /* [in] */ ULARGE_INTEGER cb, /* [out] */ ULARGE_INTEGER *pcbRead, /* [out] */ ULARGE_INTEGER *pcbWritten) = 0; virtual HRESULT STDMETHODCALLTYPE Commit( /* [in] */ DWORD grfCommitFlags) = 0; virtual HRESULT STDMETHODCALLTYPE Revert( void) = 0; virtual HRESULT STDMETHODCALLTYPE LockRegion( /* [in] */ ULARGE_INTEGER libOffset, /* [in] */ DWORD dwLockType) = 0; virtual HRESULT STDMETHODCALLTYPE UnlockRegion( virtual HRESULT STDMETHODCALLTYPE Stat( /* [out] */ __RPC__out STATSTG *pstatstg, /* [in] */ DWORD grfStatFlag) = 0; virtual HRESULT STDMETHODCALLTYPE Clone( /* [out] */ __RPC__deref_out_opt IStream **ppstm) = 0; }; |
IStream的用法跟.Net裡面的System.IO.Stream的用法類似,其中IStream::Commit函數的作用就是将記憶體中的修改儲存到硬碟中。
一般來說,結構化存儲檔案的“檔案夾”IStorage裡面都會有一個IStream儲存該“檔案夾”的目錄—即說明“檔案夾”裡面有哪些檔案。
Thumbs.db檔案的檔案描述
既然我們已經知道IStorage和IStream的概念和用法了,回過頭來看看Thumbs.db檔案,Thumbs.db檔案中有一個名稱為“Catalog”的 IStream儲存了整個Thumbs.db檔案裡面緩存的縮略圖的檔案名清單。
它包含兩段内容,第一段内容的結構叫做CatalogHeader(當然這也是我們随便取的—因為微軟并沒有公開Thumbs.db的API),儲存了所有縮略圖的大小,是32x32的,還是64x64之類的,另外還有一個重要的變量儲存了縮略圖檔案的個數。下面是這個資料結構的聲明,因為沒有對應的COM API,是以我們直接在C#中聲明了。
[Interop.StructLayout(Interop.LayoutKind.Sequential)] public struct CatalogHeader public short Reserved1; public short Reserved2; public int ThumbCount; public int ThumbWidth; public int ThumbHeight; } |
注意聲明上面的StructLayout屬性,由于.Net是即時編譯的系統,在編譯的過程當中,通常情況下,JIT會根據目前系統記憶體和CPU的架構,為結構生成最優的記憶體布局以便在通路結構體的時候能夠達到最快的速度—是以JIT可能會調整結構的一些成員在記憶體布局的順序。 由于我們是在讀取COM生成的資料,C++編譯器可沒有做到這一點,是以LayoutKind.Sequential告訴JIT編譯器,不要随意更改結構成員在記憶體中的布局。而ReveredX屬性的存在是因為這個結構是我們猜的結構,前兩個屬性沒猜出來。
第二段内容就是縮略圖的“檔案名”資訊了,除了名字以外,還儲存了縮略圖生成的時間—以便同名檔案更新的時候可以生成新的縮略圖,還有一個莫名其妙的 ItemId—估計是用來提高檢索縮略圖速度的,當然還有兩個沒猜出來的屬性。下面是這個成員的結構定義:
public struct CatalogItem public int Reserved1; private int m_ItemId; public int ItemId { get { return m_ItemId; } set { m_ItemId = value; BuildItemIdString(m_ItemId); } } public DateTime Modified; public string FileName; // 自己添加的新域 public string ItemIdString get; private set; private void BuildItemIdString(int itemId) var temp = itemId.ToString(); var buffer = new char[temp.Length]; for (int i = 0; i < temp.Length; ++i) buffer[i] = temp[temp.Length - i - 1]; ItemIdString = new string(buffer); |
不知道是什麼原因,在Thumbs.db檔案當中,資料都是以倒序儲存的,比如字元串就是倒序的, 而整形的四個位元組也是倒序排列的—難道微軟真的不想讓第三方程式員通路Thumbs.db檔案?
Thumbs.db檔案的讀取
既然已經知道檔案結構,通路的方式就不多講了,無非就是先用StgOpenStorage函數打開結構化存儲檔案,擷取IStorage接口的引用,讀取“Catalog”獲得Thumbs.db檔案的目錄,接着獲得每一個縮略圖“檔案名”對應的CatalogItem,使用CatalogItem的倒序ItemId拿到具體縮略圖的IStream指針,然後通過IStream::Read的方法來讀取縮略圖的内容,最後顯示在窗體上。唯一要注意的是,每一個縮略圖IStream的前12個位元組(3個整形)不是縮略圖的内容,不能用的,是以在讀取的時候跳過那三個位元組好了。
因為.Net隻提供了IStream的定義,而IStorage的定義需要我們自己生成。這個接口手工編寫.Net對應的接口有點麻煩