What a drag: Dragging a virtual file (HGLOBAL edition) - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20080318-00/?p=23083
Raymond Chen?2008年03月18日
拖拽虛擬文件(HGLOBAL 版本)
????????現在我們已經對簡單的數據對象有所了解,讓我們來做點稍微復雜但極其有用的事情:拖拽一個虛擬文件。實現這一功能的方法有很多,但我將從最簡單的方法開始,即虛擬文件以內存塊的形式表示。
????????記住,這個系列的副標題是“這是你能做的最少”。你可以(甚至應該)做很多可選的事情,但我將從絕對最小化的部分開始。
????????對我們一直在研究的拖拽/放置程序進行以下更改。首先,更改數據類型的枚舉:
enum {DATA_FILEGROUPDESCRIPTOR,DATA_FILECONTENTS,DATA_NUM,DATA_INVALID = -1,
};
????????拖拽虛擬文件的核心剪貼板格式是 FILEGROUPDESCRIPTOR
,它描述了正在拖拽的文件數量以及它們的各種信息。對于文件組描述符中的每個文件,你必須提供相關的文件內容,由 CFSTR_FILECONTENTS
剪貼板格式表示。
CTinyDataObject::CTinyDataObject() : m_cRef(1)
{SetFORMATETC(&m_rgfe[DATA_FILEGROUPDESCRIPTOR],RegisterClipboardFormat(CFSTR_FILEDESCRIPTOR));SetFORMATETC(&m_rgfe[DATA_FILECONTENTS],RegisterClipboardFormat(CFSTR_FILECONTENTS),TYMED_HGLOBAL, /* lindex */ 0);
}
????????初始化文件組描述符條目和之前看到的差不多。注意,結構體稱為 FILEGROUPDESCRIPTOR
,但剪貼板格式是 CFSTR_FILEDESCRIPTOR
而不是“group”。這可能最初是一個印刷錯誤,但現在我們只能接受它。
????????文件內容條目有一個轉折:lindex
是零,而不是 -1
。文件內容剪貼板格式使用 lindex
作為從零開始的索引,選擇調用者所談論的是哪個虛擬文件。由于我們只有一個虛擬文件,它的索引是零。
????????和以前一樣,所有真正的工作都在數據對象的核心,即 IDataObject::GetData
方法中。
HRESULT CTinyDataObject::GetData(FORMATETC *pfe, STGMEDIUM *pmed)
{ZeroMemory(pmed, sizeof(*pmed));switch (GetDataIndex(pfe)) {case DATA_FILEGROUPDESCRIPTOR:{FILEGROUPDESCRIPTOR fgd;ZeroMemory(&fgd, sizeof(fgd));fgd.cItems = 1;StringCchCopy(fgd.fgd[0].cFileName,ARRAYSIZE(fgd.fgd[0].cFileName),TEXT("Dummy"));pmed->tymed = TYMED_HGLOBAL;return CreateHGlobalFromBlob(&fgd, sizeof(fgd),GMEM_MOVEABLE, &pmed->hGlobal);}case DATA_FILECONTENTS:pmed->tymed = TYMED_HGLOBAL;return CreateHGlobalFromBlob("Dummy", 5,GMEM_MOVEABLE, &pmed->hGlobal);}return DV_E_FORMATETC;
}
????????當調用者請求文件組描述符時,我們填寫一個 FILEGROUPDESCRIPTOR
結構體,在填寫之前清零我們不關心的字節,以避免信息泄露漏洞。正如我指出的,我們從做絕對最小的必要事情開始,這在虛擬文件傳輸的情況下僅僅包括指定有多少虛擬文件以及它們的名稱。
當調用者請求文件零(我們唯一擁有的)的內容時,我們生成一個包含“Dummy”這個詞的五字節內存塊。
????????運行這個程序,并將不可見的對象拖出客戶端區域并將其放置到桌面上。哇,你的虛擬文件已經被復制到桌面上,并變成了一個真實的文件。(你甚至可以將它拖放到Outlook郵件撰寫窗口中,它將作為附件出現!)
????????這里還有一些問題,但我們已經至少完成了拖拽由內存塊表示的虛擬文件的絕對最小必要事項。讓我們看看其中一些可選特性,其中一些對您和最終用戶都有重大影響。
????????首先,您可能已經注意到創建的 Dummy 文件在末尾可能有一些垃圾字節。我說“可能”,因為這些垃圾字節的存在取決于堆管理器的感受。如果您只提供 HGLOBAL
,則內存塊大小的唯一指示是 GlobalSize
函數的輸出。但 GlobalSize
函數返回的大小不需要等于傳遞給 GlobalAlloc
的大小;唯一的保證是它至少和請求的大小一樣大。它可能更大,這是由于內部堆管理,例如將所有分配舍入到最近的16字節的倍數。如果進行了這樣的舍入,那么創建的 Dummy 文件將包含那些額外的垃圾字節。
????????為了避免這個問題,在 FILEGROUPDESCRIPTOR
中設置 FD_FILESIZE
標志,并在 nFileSizeLow
和 nFileSizeHigh
成員中指定確切的文件大小:
????????在 FILEGROUPDESCRIPTOR
中指定文件大小也有利于最終用戶,因為它為文件復制進度條提供了它應該接收多少字節的信息。沒有它,進度條不知道那個虛擬文件中有多少字節。它最終在請求文件內容時找到,但它是從每個文件復制時逐個學習的。進度對話框沒有機會預先收集這些信息,以提供有意義的進度反饋。
????????另一個可選細節,您可能希望利用的是,在 FILEGROUPDESCRIPTOR
中指定文件屬性和修改時間。例如,您可能希望在復制時使文件隱藏,或者您可能希望自定義最后修改時間。
????????我們來做一些事情。我們將在文件組描述符中指定文件大小以避免垃圾并改善進度反饋,并將最后修改時間設置為特定日期。
case DATA_FILEGROUPDESCRIPTOR:
{FILEGROUPDESCRIPTOR fgd;ZeroMemory(&fgd, sizeof(fgd));fgd.cItems = 1;fgd.fgd[0].dwFlags = FD_FILESIZE | FD_WRITESTIME;fgd.fgd[0].nFileSizeLow = 5;fgd.fgd[0].ftLastWriteTime.dwLowDateTime = 0x256d4000;fgd.fgd[0].ftLastWriteTime.dwHighDateTime = 0x01bf53eb;StringCchCopy(fgd.fgd[0].cFileName,ARRAYSIZE(fgd.fgd[0].cFileName),TEXT("Dummy"));pmed->tymed = TYMED_HGLOBAL;return CreateHGlobalFromBlob(&fgd, sizeof(fgd),GMEM_MOVEABLE, &pmed->hGlobal);
}
????????現在,當您放置文件時,它在末尾將沒有任何垃圾字節,時間戳將是2000年1月1日午夜UTC。(由于文件太小,您不會注意到進度條有任何改進。)
????????盡管我們沒有做很多,但這對許多人來說可能已經足夠了,尤其是那些只想允許用戶從他們的程序中拖拽一個對象并將其放入資源管理器窗口以創建相應文件的人,只要 HGLOBAL
是文件內容的方便格式。這對于小文件是合適的,但隨著文件變大,您必須一次性生成整個文件的事實可能變得昂貴。下次我們將看看一個替代方案。