一般在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的源代碼,上述內容只不過是我的一個小猜想而已):
結構化存儲文件的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,
/* [in] */ DWORD grfMode,
/* [in] */ DWORD reserved2,
/* [out] */ IStream **ppstm) = 0;
virtual HRESULT STDMETHODCALLTYPE CreateStorage(
/* [string][in] */ __RPC__in const OLECHAR *pwcsName,
/* [in] */ DWORD grfMode,
/* [in] */ DWORD reserved1,
/* [in] */ DWORD reserved2,
/* [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,
/* [in] */ DWORD grfMode,
/* [unique][in] */ __RPC__deref_opt_in_opt SNB snbExclude,
/* [in] */ DWORD reserved,
/* [out] */ __RPC__deref_out_opt IStorage **ppstg) = 0;
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(
/* [string][in] */ __RPC__in const OLECHAR *pwcsName,
/* [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(
/* [in] */ DWORD reserved1,
/* [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(
/* [string][unique][in] */ __RPC__in_opt const OLECHAR *pwcsName,
/* [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] */ ULARGE_INTEGER cb,
/* [in] */ DWORD dwLockType) = 0;
virtual HRESULT STDMETHODCALLTYPE UnlockRegion(
/* [in] */ ULARGE_INTEGER libOffset,
/* [in] */ ULARGE_INTEGER cb,
/* [in] */ DWORD dwLockType) = 0;
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—估計是用來提高檢索縮略圖速度的,當然還有兩個沒猜出來的屬性。下面是這個成員的結構定義:
[Interop.StructLayout(Interop.LayoutKind.Sequential)]
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 short Reserved2;
// 自己添加的新域
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對應的接口有點麻煩,因此建議去http://www.pinvoke.net/ 去搜索別人已經寫好的定義。